Browse Source

Merge pull request #11829 from thingsboard/feature/dashboard-sync

Gateways dashboard sync-up
pull/11814/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
bfb25e5fca
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      application/src/main/data/json/system/widget_types/gateway_configuration.json
  2. 10
      application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json
  3. 10
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  4. 10
      application/src/main/data/json/system/widget_types/gateway_custom_statistics.json
  5. 10
      application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json
  6. 10
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  7. 10
      application/src/main/data/json/system/widget_types/gateway_logs.json
  8. 10
      application/src/main/data/json/system/widget_types/service_rpc.json
  9. 7031
      application/src/main/data/resources/dashboards/gateways_dashboard.json
  10. 1
      application/src/main/data/resources/js_modules/gateway-management-extension.js
  11. 70
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  12. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  13. 101
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
  14. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java
  15. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java
  16. 67
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  17. 168
      application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java
  18. 33
      application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java
  19. 11
      application/src/main/resources/thingsboard.yml
  20. 55
      application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java
  21. 2
      application/src/test/resources/application-test.properties
  22. 4
      application/src/test/resources/logback-test.xml
  23. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  24. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  25. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
  26. 3
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
  27. 4
      common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java
  28. 12
      common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java
  29. 19
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  30. 44
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  31. 6
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  32. 44
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  33. 59
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  34. 76
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
  35. 1
      pom.xml

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

@ -8,7 +8,15 @@
"type": "static",
"sizeX": 8,
"sizeY": 6.5,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\">\n</tb-gateway-form>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n",

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

@ -8,7 +8,15 @@
"type": "latest",
"sizeX": 7.5,
"sizeY": 9,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\"\n [isStateForm]=\"true\">\n</tb-gateway-form>",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
"controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n",

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

@ -8,7 +8,15 @@
"type": "latest",
"sizeX": 11,
"sizeY": 8,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>",
"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.gatewayConnectors?.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",

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

@ -8,7 +8,15 @@
"type": "timeseries",
"sizeX": 8,
"sizeY": 5,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-statistics [ctx]=ctx></tb-gateway-statistics>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() { \n};\n\nself.onDataUpdated = function() {\n};\n\nself.onLatestDataUpdated = function() {\n};\n\nself.onResize = function() {\n};\n\nself.onEditModeChanged = function() {\n};\n\nself.onDestroy = function() {\n};\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: false,\n dataKeysOptional: true\n };\n}\n",

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

@ -8,7 +8,15 @@
"type": "timeseries",
"sizeX": 8,
"sizeY": 5,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-statistics [ctx]=ctx [general]='true'></tb-gateway-statistics>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() { \n};\n\nself.onDataUpdated = function() {\n};\n\nself.onLatestDataUpdated = function() {\n};\n\nself.onResize = function() {\n};\n\nself.onEditModeChanged = function() {\n};\n\nself.onDestroy = function() {\n};\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: false\n };\n}\n",

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

@ -8,7 +8,15 @@
"type": "latest",
"sizeX": 11,
"sizeY": 8,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-configuration [device]=\"entityId\" *ngIf=\"entityId\"></tb-gateway-configuration>",
"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.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",

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

@ -8,7 +8,15 @@
"type": "timeseries",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-logs [ctx]=\"ctx\">\n \n</tb-gateway-logs>",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
"controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",

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

@ -8,7 +8,15 @@
"type": "rpc",
"sizeX": 8.5,
"sizeY": 5.5,
"resources": [],
"resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-service-rpc [ctx]=\"ctx\"></tb-gateway-service-rpc>",
"templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section.flex-1 {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n\n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}",
"controllerScript": "\nself.onInit = function() {\n};",

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

File diff suppressed because it is too large

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

File diff suppressed because one or more lines are too long

70
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -44,10 +44,12 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService;
@ -106,13 +108,29 @@ public class TbResourceController extends BaseController {
.body(resource);
}
@ApiOperation(value = "Download resource (downloadResource)",
notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/resource/{resourceType}/{scope}/{key}")
public ResponseEntity<ByteArrayResource> downloadResourceIfChanged(@Parameter(description = "Type of the resource", schema = @Schema(allowableValues = {"lwm2m_model", "jks", "pkcs_12", "js_module", "dashboard"}))
@PathVariable("resourceType") String resourceTypeStr,
@Parameter(description = "Scope of the resource", schema = @Schema(allowableValues = {"system", "tenant"}))
@PathVariable String scope,
@Parameter(description = "Key of the resource, e.g. 'extension.js'")
@PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
ResourceType resourceType = ResourceType.valueOf(resourceTypeStr.toUpperCase());
return downloadResourceIfChanged(() -> checkResourceInfo(scope, resourceType, key, Operation.READ), etag);
}
@ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml")
public ResponseEntity<ByteArrayResource> downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag);
return downloadResourceIfChanged(strResourceId, etag);
}
@ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ -121,7 +139,7 @@ public class TbResourceController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadPkcs12ResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag);
return downloadResourceIfChanged(strResourceId, etag);
}
@ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)",
@ -131,7 +149,7 @@ public class TbResourceController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadJksResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag);
return downloadResourceIfChanged(strResourceId, etag);
}
@ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@ -140,7 +158,7 @@ public class TbResourceController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadJsResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag);
return downloadResourceIfChanged(strResourceId, etag);
}
@ApiOperation(value = "Get Resource Info (getResourceInfoById)",
@ -211,6 +229,7 @@ public class TbResourceController extends BaseController {
} else {
Collections.addAll(resourceTypes, ResourceType.values());
resourceTypes.remove(ResourceType.IMAGE);
resourceTypes.remove(ResourceType.DASHBOARD);
}
filter.resourceTypes(resourceTypes);
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
@ -271,7 +290,7 @@ public class TbResourceController extends BaseController {
@RequestParam String sortOrder,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam String sortProperty,
@Parameter(description = "LwM2M Object ids.", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@Parameter(description = "LwM2M Object ids.", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam(required = false) String[] objectIds) throws ThingsboardException {
return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds));
}
@ -288,30 +307,49 @@ public class TbResourceController extends BaseController {
tbResourceService.delete(tbResource, getCurrentUser());
}
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ResourceType resourceType, String strResourceId, String etag) throws ThingsboardException {
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(String strResourceId, String etag) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
return downloadResourceIfChanged(() -> checkResourceInfoId(resourceId, Operation.READ), etag);
}
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ThrowingSupplier<TbResourceInfo> resourceInfoProvider,
String etag) throws ThingsboardException {
TbResourceInfo resourceInfo = resourceInfoProvider.get();
if (etag != null) {
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
if (etag.equals(tbResourceInfo.getEtag())) {
if (etag.equals(resourceInfo.getEtag())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(tbResourceInfo.getEtag())
.eTag(resourceInfo.getEtag())
.build();
}
}
TbResource tbResource = checkResourceId(resourceId, Operation.READ);
ByteArrayResource resource = new ByteArrayResource(tbResource.getData());
byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
ByteArrayResource resource = new ByteArrayResource(data);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
.header("x-filename", tbResource.getFileName())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resourceInfo.getFileName())
.header("x-filename", resourceInfo.getFileName())
.contentLength(resource.contentLength())
.header("Content-Type", resourceType.getMediaType())
.header("Content-Type", resourceInfo.getResourceType().getMediaType())
.cacheControl(CacheControl.noCache())
.eTag(tbResource.getEtag())
.eTag(resourceInfo.getEtag())
.body(resource);
}
private TbResourceInfo checkResourceInfo(String scope, ResourceType resourceType, String key, Operation operation) throws ThingsboardException {
TenantId tenantId;
if (scope.equals("tenant")) {
tenantId = getTenantId();
} else if (scope.equals("system")) {
tenantId = TenantId.SYS_TENANT_ID;
} else {
throw new IllegalArgumentException("Invalid scope");
}
TbResourceInfo resourceInfo = resourceService.findResourceInfoByTenantIdAndKey(tenantId, resourceType, key);
checkEntity(getCurrentUser(), checkNotNull(resourceInfo), operation);
return resourceInfo;
}
}

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

@ -154,6 +154,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes();
installScripts.loadSystemResources();
systemDataLoaderService.loadSystemWidgets();
installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages();
@ -195,6 +196,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings();
installScripts.loadSystemResources();
systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues();

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

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.entitiy.dashboard;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.GitSyncService;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@Service
@TbCoreComponent
@RequiredArgsConstructor
@Slf4j
public class DashboardSyncService {
private final GitSyncService gitSyncService;
private final ResourceService resourceService;
private final WidgetsBundleService widgetsBundleService;
private final PartitionService partitionService;
@Value("${transport.gateway.dashboard.sync.enabled:true}")
private boolean enabled;
@Value("${transport.gateway.dashboard.sync.repository_url:}")
private String repoUrl;
@Value("${transport.gateway.dashboard.sync.branch:main}")
private String branch;
@Value("${transport.gateway.dashboard.sync.fetch_frequency:24}")
private int fetchFrequencyHours;
private static final String REPO_KEY = "gateways-dashboard";
private static final String GATEWAYS_DASHBOARD_KEY = "gateways_dashboard.json";
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void init() throws Exception {
if (!enabled) {
return;
}
gitSyncService.registerSync(REPO_KEY, repoUrl, branch, TimeUnit.HOURS.toMillis(fetchFrequencyHours), this::update);
}
private void update() {
if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) {
return;
}
List<RepoFile> resources = listFiles("resources");
for (RepoFile resourceFile : resources) {
String data = getFileContent(resourceFile.path());
resourceService.updateSystemResource(ResourceType.JS_MODULE, resourceFile.name(), data);
}
Stream<String> widgetsBundles = listFiles("widget_bundles").stream()
.map(widgetsBundleFile -> getFileContent(widgetsBundleFile.path()));
Stream<String> widgetTypes = listFiles("widget_types").stream()
.map(widgetTypeFile -> getFileContent(widgetTypeFile.path()));
widgetsBundleService.updateSystemWidgets(widgetsBundles, widgetTypes);
RepoFile dashboardFile = listFiles("dashboards").get(0);
String dashboardJson = getFileContent(dashboardFile.path());
resourceService.updateSystemResource(ResourceType.DASHBOARD, GATEWAYS_DASHBOARD_KEY, dashboardJson);
log.info("Gateways dashboard sync completed");
}
private List<RepoFile> listFiles(String path) {
return gitSyncService.listFiles(REPO_KEY, path, 1, FileType.FILE);
}
private String getFileContent(String path) {
return gitSyncService.getFileContent(REPO_KEY, path);
}
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.widgets.bundle;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -34,6 +35,7 @@ import java.util.List;
@Service
@TbCoreComponent
@AllArgsConstructor
@Slf4j
public class DefaultWidgetsBundleService extends AbstractTbEntityService implements TbWidgetsBundleService {
private final WidgetsBundleService widgetsBundleService;
@ -79,4 +81,5 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme
widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns);
autoCommit(user, widgetsBundleId);
}
}

1
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java

@ -29,4 +29,5 @@ public interface TbWidgetsBundleService extends SimpleTbEntityService<WidgetsBun
void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception;
}

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

@ -37,13 +37,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -57,6 +53,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
@ -95,7 +92,7 @@ public class InstallScripts {
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards";
public static final String MODELS_LWM2M_DIR = "lwm2m-registry";
public static final String CREDENTIALS_DIR = "credentials";
public static final String RESOURCES_DIR = "resources";
public static final String JSON_EXT = ".json";
public static final String SVG_EXT = ".svg";
@ -355,7 +352,7 @@ public class InstallScripts {
}
private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol,
ImageUtils.ScadaSymbolMetadataInfo metadata) {
ImageUtils.ScadaSymbolMetadataInfo metadata) {
String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink();
WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails();
JsonNode descriptor = JacksonUtil.clone(template.getDescriptor());
@ -375,34 +372,26 @@ public class InstallScripts {
defaultConfig.put("title", metadata.getTitle());
ObjectNode settings;
if (defaultConfig.has("settings")) {
settings = (ObjectNode)defaultConfig.get("settings");
settings = (ObjectNode) defaultConfig.get("settings");
} else {
settings = JacksonUtil.newObjectNode();
defaultConfig.set("settings", settings);
}
settings.put("scadaSymbolUrl", symbolUrl);
((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX());
((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY());
((ObjectNode) descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
((ObjectNode) descriptor).put("sizeX", metadata.getWidgetSizeX());
((ObjectNode) descriptor).put("sizeY", metadata.getWidgetSizeY());
String controllerScript = descriptor.get("controllerScript").asText();
controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'");
controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'");
((ObjectNode)descriptor).put("controllerScript", controllerScript);
((ObjectNode) descriptor).put("controllerScript", controllerScript);
return widgetTypeService.saveWidgetType(scadaSymbolWidget);
}
private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
PageData<WidgetTypeInfo> widgetTypes;
var pageLink = new PageLink(1024);
do {
widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), false, DeprecatedFilter.ALL, null, pageLink);
for (var widgetType : widgetTypes.getData()) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
pageLink.nextPageLink();
} while (widgetTypes.hasNext());
widgetTypeService.deleteWidgetTypesByBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
}
}
@ -419,7 +408,7 @@ public class InstallScripts {
public void loadSystemImages() {
log.info("Loading system images...");
Stream<Path> dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
try (dashboardsFiles) {
dashboardsFiles.forEach(file -> {
try {
@ -523,6 +512,41 @@ public class InstallScripts {
}
}
public void loadSystemResources() {
Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR);
loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE);
loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD);
}
private void loadSystemResources(Path dir, ResourceType resourceType) {
listDir(dir).forEach(resourceFile -> {
String resourceKey = resourceFile.getFileName().toString();
try {
String data = getContent(resourceFile);
TbResource resource = resourceService.updateSystemResource(resourceType, resourceKey, data);
log.info("{} resource {}", (resource.getId() == null ? "Created" : "Updated"), resourceKey);
} catch (Exception e) {
throw new RuntimeException("Unable to load system resource " + resourceFile, e);
}
});
}
private String getContent(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private Stream<Path> listDir(Path resourcesDir) {
try {
return Files.list(resourcesDir);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void doSaveLwm2mResource(TbResource resource) throws ThingsboardException {
log.trace("Executing saveResource [{}]", resource);
if (resource.getData() == null || resource.getData().length == 0) {
@ -534,4 +558,5 @@ public class InstallScripts {
resourceService.saveResource(resource);
}
}
}

168
application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java

@ -0,0 +1,168 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.GitRepository;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@Slf4j
public class DefaultGitSyncService implements GitSyncService {
@Value("${vc.git.repositories-folder:${java.io.tmpdir}/repositories}")
private String repositoriesFolder;
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("git-sync"));
private final Map<String, GitRepository> repositories = new ConcurrentHashMap<>();
private final Map<String, Runnable> updateListeners = new ConcurrentHashMap<>();
@Override
public void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate) {
RepositorySettings settings = new RepositorySettings();
settings.setRepositoryUri(repoUri);
settings.setDefaultBranch(branch);
if (onUpdate != null) {
updateListeners.put(key, onUpdate);
}
executor.execute(() -> {
initRepository(key, settings);
});
executor.scheduleWithFixedDelay(() -> {
GitRepository repository = repositories.get(key);
if (repository == null || !GitRepository.exists(repository.getDirectory())) {
initRepository(key, settings);
return;
}
try {
log.debug("[{}] Fetching repository", key);
repository.fetch();
onUpdate(key);
} catch (Throwable e) {
log.error("[{}] Failed to fetch repository", key, e);
}
}, fetchFrequencyMs, fetchFrequencyMs, TimeUnit.MILLISECONDS);
}
@Override
public List<RepoFile> listFiles(String key, String path, int depth, FileType type) {
GitRepository repository = getRepository(key);
return repository.listFilesAtCommit(getBranchRef(repository), path, depth).stream()
.filter(file -> type == null || file.type() == type)
.toList();
}
@Override
public String getFileContent(String key, String path) {
GitRepository repository = getRepository(key);
try {
return repository.getFileContentAtCommit(path, getBranchRef(repository));
} catch (Exception e) {
log.warn("[{}] Failed to get file content for path {}: {}", key, path, e.getMessage());
return "{}";
}
}
@Override
public String getGithubRawContentUrl(String key, String path) {
if (path == null) {
return "";
}
RepositorySettings settings = getRepository(key).getSettings();
return StringUtils.removeEnd(settings.getRepositoryUri(), ".git") + "/blob/" + settings.getDefaultBranch() + "/" + path + "?raw=true";
}
private GitRepository getRepository(String key) {
GitRepository repository = repositories.get(key);
if (repository != null) {
if (!GitRepository.exists(repository.getDirectory())) {
// reinitializing the repository because folder was deleted
initRepository(key, repository.getSettings());
}
}
repository = repositories.get(key);
if (repository == null) {
throw new IllegalStateException(key + " repository is not initialized");
}
return repository;
}
private void initRepository(String key, RepositorySettings settings) {
try {
repositories.remove(key);
Path directory = getRepoDirectory(settings);
GitRepository repository = GitRepository.openOrClone(directory, settings, true);
repositories.put(key, repository);
log.info("[{}] Initialized repository", key);
onUpdate(key);
} catch (Throwable e) {
log.error("[{}] Failed to initialize repository with settings {}", key, settings, e);
}
}
private void onUpdate(String key) {
Runnable listener = updateListeners.get(key);
if (listener != null) {
log.debug("[{}] Handling repository update", key);
try {
listener.run();
} catch (Throwable e) {
log.error("[{}] Failed to handle repository update", key, e);
}
}
}
private Path getRepoDirectory(RepositorySettings settings) {
// using uri to define folder name in case repo url is changed
String name = URI.create(settings.getRepositoryUri()).getPath().replaceAll("[^a-zA-Z]", "");
return Path.of(repositoriesFolder, name);
}
private String getBranchRef(GitRepository repository) {
return "refs/remotes/origin/" + repository.getSettings().getDefaultBranch();
}
@PreDestroy
private void preDestroy() {
executor.shutdownNow();
}
}

33
application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
import java.util.List;
public interface GitSyncService {
void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate);
List<RepoFile> listFiles(String key, String path, int depth, FileType type);
String getFileContent(String key, String path);
String getGithubRawContentUrl(String key, String path);
}

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

@ -1244,6 +1244,17 @@ transport:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
# Interval of transport statistics logging
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
gateway:
dashboard:
sync:
# Enable/disable gateways dashboard sync with git repository
enabled: "${TB_GATEWAY_DASHBOARD_SYNC_ENABLED:true}"
# URL of gateways dashboard repository
repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}"
# Branch of gateways dashboard repository to work with
branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:main}"
# Fetch frequency in hours for gateways dashboard repository
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
# CoAP server parameters
coap:

55
application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.entitiy.dashboard;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"transport.gateway.dashboard.sync.enabled=true"
})
public class DashboardSyncServiceTest extends AbstractControllerTest {
@Test
public void testGatewaysDashboardSync() throws Exception {
loginTenantAdmin();
await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> {
MockHttpServletResponse response = doGet("/api/resource/dashboard/system/gateways_dashboard.json")
.andExpect(status().isOk())
.andReturn().getResponse();
String dashboardJson = response.getContentAsString();
String etag = response.getHeader("ETag");
Dashboard dashboard = JacksonUtil.fromString(dashboardJson, Dashboard.class);
assertThat(dashboard).isNotNull();
assertThat(dashboard.getTitle()).containsIgnoringCase("gateway");
assertThat(etag).isNotBlank();
});
}
}

2
application/src/test/resources/application-test.properties

@ -54,3 +54,5 @@ sql.edge_events.partition_size=168
sql.ttl.edge_events.edge_event_ttl=2592000
server.log_controller_error_stack_trace=false
transport.gateway.dashboard.sync.enabled=false

4
application/src/test/resources/logback-test.xml

@ -17,7 +17,9 @@
<logger name="org.eclipse.leshan" level="INFO"/>
<logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/>
<logger name="org.thingsboard.server.service.script" level="INFO"/>
<logger name="org.thingsboard.server.service.entitiy.dashboard" level="TRACE"/>
<logger name="org.thingsboard.server.service.entitiy.widgets" level="TRACE"/>
<logger name="org.thingsboard.server.service.sync" level="TRACE"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>

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

@ -38,6 +38,8 @@ public interface ResourceService extends EntityDaoService {
TbResource findResourceById(TenantId tenantId, TbResourceId resourceId);
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
@ -62,4 +64,8 @@ public interface ResourceService extends EntityDaoService {
long sumDataSizeByTenantId(TenantId tenantId);
TbResource updateSystemResource(ResourceType resourceType, String resourceKey, String data);
String checkSystemResourcesUsage(String content, ResourceType... usedResourceTypes);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -65,4 +65,6 @@ public interface WidgetTypeService extends EntityDaoService {
void deleteWidgetTypesByTenantId(TenantId tenantId);
void deleteWidgetTypesByBundleId(TenantId tenantId, WidgetsBundleId bundleId);
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
import java.util.stream.Stream;
public interface WidgetsBundleService extends EntityDaoService {
@ -49,4 +50,6 @@ public interface WidgetsBundleService extends EntityDaoService {
void deleteWidgetsBundlesByTenantId(TenantId tenantId);
void updateSystemWidgets(Stream<String> bundles, Stream<String> widgets);
}

3
common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java

@ -24,7 +24,8 @@ public enum ResourceType {
JKS("application/x-java-keystore", false, false),
PKCS_12("application/x-pkcs12", false, false),
JS_MODULE("application/javascript", true, true),
IMAGE(null, true, true);
IMAGE(null, true, true),
DASHBOARD("application/json", true, true);
@Getter
private final String mediaType;

4
common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java

@ -15,9 +15,11 @@
*/
package org.thingsboard.server.common.data.util;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@FunctionalInterface
public interface ThrowingSupplier<T> {
T get() throws Exception;
T get() throws ThingsboardException;
}

12
common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java

@ -17,16 +17,24 @@ package org.thingsboard.common.util;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.intellij.lang.annotations.Language;
import org.springframework.util.ConcurrentReferenceHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RegexUtils {
public static final Pattern UUID_PATTERN = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
private static final ConcurrentMap<String, Pattern> patternsCache = new ConcurrentReferenceHashMap<>(16, SOFT);
public static String replace(String s, Pattern pattern, UnaryOperator<String> replacer) {
return pattern.matcher(s).replaceAll(matchResult -> {
@ -34,6 +42,10 @@ public class RegexUtils {
});
}
public static String replace(String input, @Language("regexp") String pattern, Function<MatchResult, String> replacer) {
return patternsCache.computeIfAbsent(pattern, Pattern::compile).matcher(input).replaceAll(replacer);
}
public static boolean matches(String input, Pattern pattern) {
return pattern.matcher(input).matches();
}

19
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java

@ -40,7 +40,6 @@ import org.thingsboard.server.service.sync.vc.GitRepository.Diff;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
@ -283,23 +282,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
private GitRepository openOrCloneRepository(TenantId tenantId, RepositorySettings settings, boolean fetch) throws Exception {
log.debug("[{}] Init tenant repository started.", tenantId);
Path repositoryDirectory = Path.of(repositoriesFolder, settings.isLocalOnly() ? "local_" + settings.getRepositoryUri() : tenantId.getId().toString());
GitRepository repository;
if (GitRepository.exists(repositoryDirectory.toString())) {
repository = GitRepository.open(repositoryDirectory.toFile(), settings);
if (fetch) {
repository.fetch();
}
} else {
FileUtils.deleteDirectory(repositoryDirectory.toFile());
Files.createDirectories(repositoryDirectory);
if (settings.isLocalOnly()) {
repository = GitRepository.create(settings, repositoryDirectory.toFile());
} else {
repository = GitRepository.clone(settings, repositoryDirectory.toFile());
}
}
GitRepository repository = GitRepository.openOrClone(repositoryDirectory, settings, fetch);
repositories.put(tenantId, repository);
log.debug("[{}] Init tenant repository completed.", tenantId);
return repository;

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

@ -138,6 +138,25 @@ public class GitRepository {
return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
}
public static GitRepository openOrClone(Path directory, RepositorySettings settings, boolean fetch) throws IOException, GitAPIException {
GitRepository repository;
if (GitRepository.exists(directory.toString())) {
repository = GitRepository.open(directory.toFile(), settings);
if (fetch) {
repository.fetch();
}
} else {
FileUtils.deleteDirectory(directory.toFile());
Files.createDirectories(directory);
if (settings.isLocalOnly()) {
repository = GitRepository.create(settings, directory.toFile());
} else {
repository = GitRepository.clone(settings, directory.toFile());
}
}
return repository;
}
public static void test(RepositorySettings settings, File directory) throws Exception {
if (settings.isLocalOnly()) {
return;
@ -235,22 +254,29 @@ public class GitRepository {
return iterableToPageData(commits, this::toCommit, pageLink, revCommitComparatorFunction);
}
public List<String> listFilesAtCommit(String commitId) throws IOException {
return listFilesAtCommit(commitId, null);
public List<String> listFilesAtCommit(String commitId, String path) {
return listFilesAtCommit(commitId, path, -1).stream().map(RepoFile::path).toList();
}
public List<String> listFilesAtCommit(String commitId, String path) throws IOException {
@SneakyThrows
public List<RepoFile> listFilesAtCommit(String commitId, String path, int depth) {
log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, path);
List<String> files = new ArrayList<>();
List<RepoFile> files = new ArrayList<>();
RevCommit revCommit = resolveCommit(commitId);
try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) {
treeWalk.reset(revCommit.getTree().getId());
if (StringUtils.isNotEmpty(path)) {
treeWalk.setFilter(PathFilter.create(path));
}
treeWalk.setRecursive(true);
boolean fixedDepth = depth != -1;
treeWalk.setRecursive(!fixedDepth);
while (treeWalk.next()) {
files.add(treeWalk.getPathString());
if (!fixedDepth || treeWalk.getDepth() == depth) {
files.add(new RepoFile(treeWalk.getPathString(), treeWalk.getNameString(), treeWalk.isSubtree() ? FileType.DIRECTORY : FileType.FILE));
}
if (fixedDepth && treeWalk.getDepth() < depth) {
treeWalk.enterSubtree();
}
}
}
return files;
@ -592,4 +618,10 @@ public class GitRepository {
private String diffStringValue;
}
public record RepoFile(String path, String name, FileType type) {}
public enum FileType {
FILE, DIRECTORY
}
}

6
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -248,8 +248,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
@Override
public byte[] getImageData(TenantId tenantId, TbResourceId imageId) {
log.trace("Executing getImageData [{}] [{}]", tenantId, imageId);
return resourceDao.getResourceData(tenantId, imageId);
return getResourceData(tenantId, imageId);
}
@Override
@ -448,7 +447,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
mdResourceName = new String(Base64.getDecoder().decode(matcher.group(2)), StandardCharsets.UTF_8);
if (StringUtils.isNotBlank(matcher.group(3))) {
mdResourceSubType = new String(Base64.getDecoder().decode(matcher.group(3)), StandardCharsets.UTF_8);
};
}
mdMediaType = matcher.group(4);
} else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) {
mdMediaType = StringUtils.substringBetween(data, "data:", ";base64");
@ -687,4 +686,5 @@ public class BaseImageService extends BaseResourceService implements ImageServic
private final boolean updated;
private final String value;
}
}

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

@ -23,6 +23,7 @@ import org.hibernate.exception.ConstraintViolationException;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.RegexUtils;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.EntityType;
@ -44,6 +45,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
@ -120,6 +122,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceDao.findById(tenantId, resourceId.getId());
}
@Override
public byte[] getResourceData(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing getResourceData [{}] [{}]", tenantId, resourceId);
return resourceDao.getResourceData(tenantId, resourceId);
}
@Override
public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
@ -231,6 +239,41 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceDao.sumDataSizeByTenantId(tenantId);
}
@Override
public TbResource updateSystemResource(ResourceType resourceType, String resourceKey, String data) {
if (resourceType == ResourceType.DASHBOARD) {
data = checkSystemResourcesUsage(data, ResourceType.JS_MODULE);
}
TbResource resource = findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey);
if (resource == null) {
resource = new TbResource();
resource.setTenantId(TenantId.SYS_TENANT_ID);
resource.setResourceType(resourceType);
resource.setResourceKey(resourceKey);
resource.setFileName(resourceKey);
resource.setTitle(resourceKey);
}
resource.setData(data.getBytes(StandardCharsets.UTF_8));
log.debug("{} system resource {}", (resource.getId() == null ? "Creating" : "Updating"), resourceKey);
return saveResource(resource);
}
@Override
public String checkSystemResourcesUsage(String content, ResourceType... usedResourceTypes) {
return RegexUtils.replace(content, "\\$\\{RESOURCE:(.+)}", matchResult -> {
String resourceKey = matchResult.group(1);
for (ResourceType resourceType : usedResourceTypes) {
TbResourceInfo resource = findResourceInfoByTenantIdAndKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey);
if (resource != null) {
log.trace("Replaced '{}' with resource id {}", matchResult.group(), resource.getUuidId());
return resource.getUuidId().toString();
}
}
return "";
});
}
protected String calculateEtag(byte[] data) {
return Hashing.sha256().hashBytes(data).toString();
}
@ -256,4 +299,5 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId()));
}
}
}

59
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -44,7 +44,6 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -218,7 +217,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
List<WidgetTypeId> toRemove = existingBundleWidgets.stream()
.map(WidgetsBundleWidget::getWidgetTypeId)
.filter(widgetTypeId -> bundleWidgets.stream().noneMatch(newBundleWidget ->
newBundleWidget.getWidgetTypeId().equals(widgetTypeId))).collect(Collectors.toList());
newBundleWidget.getWidgetTypeId().equals(widgetTypeId))).toList();
for (WidgetTypeId widgetTypeId : toRemove) {
widgetTypeDao.removeWidgetTypeFromWidgetsBundle(widgetsBundleId.getId(), widgetTypeId.getId());
}
@ -243,6 +242,12 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
tenantWidgetTypeRemover.removeEntities(tenantId, tenantId);
}
@Override
public void deleteWidgetTypesByBundleId(TenantId tenantId, WidgetsBundleId bundleId) {
log.trace("Executing deleteWidgetTypesByBundleId, tenantId [{}], bundleId [{}]", tenantId, bundleId);
bundleWidgetTypesRemover.removeEntities(tenantId, bundleId);
}
@Override
public void deleteByTenantId(TenantId tenantId) {
deleteWidgetTypesByTenantId(tenantId);
@ -258,24 +263,36 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return EntityType.WIDGET_TYPE;
}
private PaginatedRemover<TenantId, WidgetTypeInfo> tenantWidgetTypeRemover =
new PaginatedRemover<>() {
@Override
protected PageData<WidgetTypeInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetTypeDao.findTenantWidgetTypesByTenantId(
WidgetTypeFilter.builder()
.tenantId(id)
.fullSearch(false)
.deprecatedFilter(DeprecatedFilter.ALL)
.widgetTypes(null).build(),
pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) {
deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId()));
}
};
private final PaginatedRemover<TenantId, WidgetTypeInfo> tenantWidgetTypeRemover = new PaginatedRemover<>() {
@Override
protected PageData<WidgetTypeInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetTypeDao.findTenantWidgetTypesByTenantId(
WidgetTypeFilter.builder()
.tenantId(id)
.fullSearch(false)
.deprecatedFilter(DeprecatedFilter.ALL)
.widgetTypes(null).build(),
pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) {
deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId()));
}
};
private final PaginatedRemover<WidgetsBundleId, WidgetTypeInfo> bundleWidgetTypesRemover = new PaginatedRemover<>() {
@Override
protected PageData<WidgetTypeInfo> findEntities(TenantId tenantId, WidgetsBundleId widgetsBundleId, PageLink pageLink) {
return findWidgetTypesInfosByWidgetsBundleId(tenantId, widgetsBundleId, false, DeprecatedFilter.ALL, null, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, WidgetTypeInfo widgetTypeInfo) {
deleteWidgetType(tenantId, widgetTypeInfo.getId());
}
};
}

76
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java

@ -15,17 +15,23 @@
*/
package org.thingsboard.server.dao.widget;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
@ -33,6 +39,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -40,6 +47,7 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@Service("WidgetsBundleDaoService")
@Slf4j
@ -64,6 +72,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
@Autowired
private ImageService imageService;
@Autowired
private ResourceService resourceService;
@Override
public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) {
log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId);
@ -196,6 +207,71 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
deleteWidgetsBundlesByTenantId(tenantId);
}
@Transactional
@Override
public void updateSystemWidgets(Stream<String> bundles, Stream<String> widgets) {
widgets.forEach(widgetTypeJson -> {
try {
updateSystemWidget(widgetTypeJson);
} catch (Exception e) {
throw new RuntimeException("Unable to load widget type from json: " + widgetTypeJson, e);
}
});
bundles.forEach(widgetsBundleDescriptorJson -> {
JsonNode widgetsBundleDescriptor = JacksonUtil.toJsonNode(widgetsBundleDescriptorJson);
if (widgetsBundleDescriptor == null || !widgetsBundleDescriptor.has("widgetsBundle")) {
throw new RuntimeException("Invalid widgets bundle json: [" + widgetsBundleDescriptorJson + "]");
}
JsonNode widgetsBundleJson = widgetsBundleDescriptor.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle existingWidgetsBundle = findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, widgetsBundle.getAlias());
if (existingWidgetsBundle != null) {
widgetsBundle.setId(existingWidgetsBundle.getId());
widgetsBundle.setCreatedTime(existingWidgetsBundle.getCreatedTime());
}
widgetsBundle.setTenantId(TenantId.SYS_TENANT_ID);
widgetsBundle = saveWidgetsBundle(widgetsBundle);
log.debug("{} widgets bundle {}", existingWidgetsBundle == null ? "Created" : "Updated", widgetsBundle.getAlias());
List<String> widgetTypeFqns = new ArrayList<>();
if (widgetsBundleDescriptor.has("widgetTypes")) {
JsonNode widgetTypesArrayJson = widgetsBundleDescriptor.get("widgetTypes");
widgetTypesArrayJson.forEach(widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = updateSystemWidget(widgetTypeJson.toString());
widgetTypeFqns.add(widgetTypeDetails.getFqn());
} catch (Exception e) {
throw new RuntimeException("Unable to load widget type from json: " + widgetsBundleDescriptorJson, e);
}
});
}
if (widgetsBundleDescriptor.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptor.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), widgetTypeFqns);
});
}
private WidgetTypeDetails updateSystemWidget(String widgetTypeJson) {
widgetTypeJson = resourceService.checkSystemResourcesUsage(widgetTypeJson, ResourceType.JS_MODULE);
WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromString(widgetTypeJson, WidgetTypeDetails.class);
WidgetType existingWidget = widgetTypeService.findWidgetTypeByTenantIdAndFqn(TenantId.SYS_TENANT_ID, widgetTypeDetails.getFqn());
if (existingWidget != null) {
widgetTypeDetails.setId(existingWidget.getId());
widgetTypeDetails.setCreatedTime(existingWidget.getCreatedTime());
}
widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID);
widgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails);
log.debug("{} widget type {}", existingWidget == null ? "Created" : "Updated", widgetTypeDetails.getFqn());
return widgetTypeDetails;
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId())));

1
pom.xml

@ -849,6 +849,7 @@
<exclude>.run/**</exclude>
<exclude>**/NetworkReceive.java</exclude>
<exclude>**/lwm2m-registry/**</exclude>
<exclude>src/main/data/resources/**</exclude>
</excludes>
<mapping>
<proto>JAVADOC_STYLE</proto>

Loading…
Cancel
Save