Browse Source

Gateways dashboard sync; new 'DASHBOARD' resource type

pull/11829/head
ViacheslavKlimov 2 years ago
parent
commit
86616e3b08
  1. 70
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  2. 126
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
  3. 74
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java
  4. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java
  5. 145
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  6. 23
      application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java
  7. 9
      application/src/main/resources/thingsboard.yml
  8. 51
      application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java
  9. 4
      application/src/test/resources/logback-test.xml
  10. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  11. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  12. 3
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
  13. 4
      common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java
  14. 6
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  15. 7
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  16. 59
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

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.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId; 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.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; 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.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
@ -106,13 +108,29 @@ public class TbResourceController extends BaseController {
.body(resource); .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) @ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml") @GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml")
public ResponseEntity<ByteArrayResource> downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) public ResponseEntity<ByteArrayResource> downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @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) @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) public ResponseEntity<ByteArrayResource> downloadPkcs12ResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @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)", @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) public ResponseEntity<ByteArrayResource> downloadJksResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @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) @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) public ResponseEntity<ByteArrayResource> downloadJsResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @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)", @ApiOperation(value = "Get Resource Info (getResourceInfoById)",
@ -211,6 +229,7 @@ public class TbResourceController extends BaseController {
} else { } else {
Collections.addAll(resourceTypes, ResourceType.values()); Collections.addAll(resourceTypes, ResourceType.values());
resourceTypes.remove(ResourceType.IMAGE); resourceTypes.remove(ResourceType.IMAGE);
resourceTypes.remove(ResourceType.DASHBOARD);
} }
filter.resourceTypes(resourceTypes); filter.resourceTypes(resourceTypes);
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
@ -271,7 +290,7 @@ public class TbResourceController extends BaseController {
@RequestParam String sortOrder, @RequestParam String sortOrder,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED)) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam String sortProperty, @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 { @RequestParam(required = false) String[] objectIds) throws ThingsboardException {
return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds));
} }
@ -288,30 +307,49 @@ public class TbResourceController extends BaseController {
tbResourceService.delete(tbResource, getCurrentUser()); 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); checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(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) { if (etag != null) {
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification 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) return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(tbResourceInfo.getEtag()) .eTag(resourceInfo.getEtag())
.build(); .build();
} }
} }
TbResource tbResource = checkResourceId(resourceId, Operation.READ); byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
ByteArrayResource resource = new ByteArrayResource(data);
ByteArrayResource resource = new ByteArrayResource(tbResource.getData());
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resourceInfo.getFileName())
.header("x-filename", tbResource.getFileName()) .header("x-filename", resourceInfo.getFileName())
.contentLength(resource.contentLength()) .contentLength(resource.contentLength())
.header("Content-Type", resourceType.getMediaType()) .header("Content-Type", resourceInfo.getResourceType().getMediaType())
.cacheControl(CacheControl.noCache()) .cacheControl(CacheControl.noCache())
.eTag(tbResource.getEtag()) .eTag(resourceInfo.getEtag())
.body(resource); .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;
}
} }

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

@ -0,0 +1,126 @@
/**
* 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 com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
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.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleService;
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.nio.charset.StandardCharsets;
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 TbWidgetsBundleService tbWidgetsBundleService;
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.fetch_frequency:24}")
private int fetchFrequencyHours;
private static final String REPO_KEY = "gateways-dashboard";
private static final String GATEWAY_RESOURCE_ID_PARAM = "${GATEWAY_RESOURCE_ID}";
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, "main", TimeUnit.HOURS.toMillis(fetchFrequencyHours), this::update);
}
private void update() {
if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) {
return;
}
RepoFile extensionResourceFile = listFiles("resources").get(0);
String data = getFileContent(extensionResourceFile.path());
TbResource extensionResource = createOrUpdateResource(ResourceType.JS_MODULE, extensionResourceFile.name(), data.getBytes(StandardCharsets.UTF_8));
String extensionResourceId = extensionResource.getUuidId().toString();
Stream<JsonNode> widgetsBundles = listFiles("widget_bundles").stream()
.map(widgetsBundleFile -> {
String widgetsBundleDescriptor = getFileContent(widgetsBundleFile.path());
widgetsBundleDescriptor = widgetsBundleDescriptor.replace(GATEWAY_RESOURCE_ID_PARAM, extensionResourceId);
return JacksonUtil.toJsonNode(widgetsBundleDescriptor);
});
Stream<JsonNode> widgetTypes = listFiles("widget_types").stream()
.map(widgetTypeFile -> {
String widgetTypeDetails = getFileContent(widgetTypeFile.path());
widgetTypeDetails = widgetTypeDetails.replace(GATEWAY_RESOURCE_ID_PARAM, extensionResourceId);
return JacksonUtil.toJsonNode(widgetTypeDetails);
});
tbWidgetsBundleService.updateWidgets(TenantId.SYS_TENANT_ID, widgetsBundles, widgetTypes);
RepoFile dashboardFile = listFiles("dashboards").get(0);
String dashboardJson = getFileContent(dashboardFile.path()).replace(GATEWAY_RESOURCE_ID_PARAM, extensionResourceId);
createOrUpdateResource(ResourceType.DASHBOARD, GATEWAYS_DASHBOARD_KEY, dashboardJson.getBytes(StandardCharsets.UTF_8));
log.info("Gateways dashboard sync completed");
}
private TbResource createOrUpdateResource(ResourceType resourceType, String resourceKey, byte[] data) {
TbResource resource = resourceService.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);
log.debug("{} resource {}", (resource.getId() == null ? "Creating" : "Updating"), resourceKey);
return resourceService.saveResource(resource);
}
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);
}
}

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

@ -15,25 +15,34 @@
*/ */
package org.thingsboard.server.service.entitiy.widgets.bundle; package org.thingsboard.server.service.entitiy.widgets.bundle;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; 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.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.id.WidgetsBundleId;
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.WidgetsBundle;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
@Service @Service
@TbCoreComponent @TbCoreComponent
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class DefaultWidgetsBundleService extends AbstractTbEntityService implements TbWidgetsBundleService { public class DefaultWidgetsBundleService extends AbstractTbEntityService implements TbWidgetsBundleService {
private final WidgetsBundleService widgetsBundleService; private final WidgetsBundleService widgetsBundleService;
@ -79,4 +88,69 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme
widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns); widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns);
autoCommit(user, widgetsBundleId); autoCommit(user, widgetsBundleId);
} }
@Transactional
@Override
public void updateWidgets(TenantId tenantId, Stream<JsonNode> bundles, Stream<JsonNode> widgets) {
widgets.forEach(widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
WidgetType existingWidget = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, widgetTypeDetails.getFqn());
if (existingWidget != null) {
widgetTypeDetails.setId(existingWidget.getId());
widgetTypeDetails.setCreatedTime(existingWidget.getCreatedTime());
}
widgetTypeDetails.setTenantId(tenantId);
widgetTypeService.saveWidgetType(widgetTypeDetails);
log.debug("{} widget type {}", existingWidget == null ? "Created" : "Updated", widgetTypeDetails.getFqn());
} catch (Exception e) {
throw new RuntimeException("Unable to load widget type from json: " + widgetTypeJson, e);
}
});
bundles.forEach(widgetsBundleDescriptorJson -> {
if (widgetsBundleDescriptorJson == null || !widgetsBundleDescriptorJson.has("widgetsBundle")) {
throw new RuntimeException("Invalid widgets bundle json: [" + widgetsBundleDescriptorJson + "]");
}
JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle existingWidgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, widgetsBundle.getAlias());
if (existingWidgetsBundle != null) {
widgetsBundle.setId(existingWidgetsBundle.getId());
widgetsBundle.setCreatedTime(existingWidgetsBundle.getCreatedTime());
}
widgetsBundle.setTenantId(tenantId);
widgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
log.debug("{} widgets bundle {}", existingWidgetsBundle == null ? "Created" : "Updated", widgetsBundle.getAlias());
List<String> widgetTypeFqns = new ArrayList<>();
if (widgetsBundleDescriptorJson.has("widgetTypes")) {
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
widgetTypesArrayJson.forEach(widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
WidgetType existingWidget = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, widgetTypeDetails.getFqn());
if (existingWidget != null) {
widgetTypeDetails.setId(existingWidget.getId());
widgetTypeDetails.setCreatedTime(existingWidget.getCreatedTime());
}
widgetTypeDetails.setTenantId(tenantId);
widgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails);
widgetTypeFqns.add(widgetTypeDetails.getFqn());
} catch (Exception e) {
throw new RuntimeException("Unable to load widget type from json: " + widgetsBundleDescriptorJson, e);
}
});
}
if (widgetsBundleDescriptorJson.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptorJson.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
}
widgetTypeService.updateWidgetsBundleWidgetFqns(tenantId, widgetsBundle.getId(), widgetTypeFqns);
});
}
} }

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

@ -15,13 +15,16 @@
*/ */
package org.thingsboard.server.service.entitiy.widgets.bundle; package org.thingsboard.server.service.entitiy.widgets.bundle;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
public interface TbWidgetsBundleService extends SimpleTbEntityService<WidgetsBundle> { public interface TbWidgetsBundleService extends SimpleTbEntityService<WidgetsBundle> {
@ -29,4 +32,6 @@ public interface TbWidgetsBundleService extends SimpleTbEntityService<WidgetsBun
void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception; void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception;
void updateWidgets(TenantId tenantId, Stream<JsonNode> bundles, Stream<JsonNode> widgets);
} }

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

@ -17,6 +17,7 @@ package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Streams;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -37,14 +38,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; 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.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; 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.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.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
@ -53,7 +49,7 @@ import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.util.ImageUtils; import org.thingsboard.server.dao.util.ImageUtils;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater; import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException; import java.io.IOException;
@ -62,9 +58,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -114,7 +108,7 @@ public class InstallScripts {
private WidgetTypeService widgetTypeService; private WidgetTypeService widgetTypeService;
@Autowired @Autowired
private WidgetsBundleService widgetsBundleService; private TbWidgetsBundleService tbWidgetsBundleService;
@Autowired @Autowired
private OAuth2ConfigTemplateService oAuth2TemplateService; private OAuth2ConfigTemplateService oAuth2TemplateService;
@ -216,91 +210,39 @@ public class InstallScripts {
public void loadSystemWidgets() throws Exception { public void loadSystemWidgets() throws Exception {
log.info("Loading system widgets"); log.info("Loading system widgets");
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
Stream<JsonNode> bundles;
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( bundles = Streams.stream(dirStream).map(path -> {
path -> { try {
JsonNode widgetsBundleDescriptorJson; return JacksonUtil.toJsonNode(path.toFile());
try { } catch (Exception e) {
widgetsBundleDescriptorJson = JacksonUtil.toJsonNode(path.toFile()); log.error("Unable to parse widgets bundle from json: [{}]", path);
} catch (Exception e) { throw new RuntimeException("Unable to parse widgets bundle from json", e);
log.error("Unable to parse widgets bundle from json: [{}]", path); }
throw new RuntimeException("Unable to parse widgets bundle from json", e); });
}
if (widgetsBundleDescriptorJson == null || !widgetsBundleDescriptorJson.has("widgetsBundle")) {
log.error("Invalid widgets bundle json: [{}]", path);
throw new RuntimeException("Invalid widgets bundle json: [" + path + "]");
}
widgetsBundlesMap.put(path, widgetsBundleDescriptorJson);
JsonNode bundleAliasNode = widgetsBundleDescriptorJson.get("widgetsBundle").get("alias");
if (bundleAliasNode == null || !bundleAliasNode.isTextual()) {
log.error("Invalid widgets bundle json: [{}]", path);
throw new RuntimeException("Invalid widgets bundle json: [" + path + "]");
}
String bundleAlias = bundleAliasNode.asText();
try {
this.deleteSystemWidgetBundle(bundleAlias);
} catch (Exception e) {
log.error("Failed to delete system widgets bundle: [{}]", bundleAlias);
throw new RuntimeException("Failed to delete system widgets bundle: [" + bundleAlias + "]", e);
}
}
);
} }
Stream<JsonNode> widgets;
Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
if (Files.exists(widgetTypesDir)) { if (Files.exists(widgetTypesDir)) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) { try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( widgets = Streams.stream(dirStream).map(path -> {
path -> { try {
try { return JacksonUtil.toJsonNode(path.toFile());
JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile()); } catch (Exception e) {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); log.error("Unable to parse widget type from json: [{}]", path);
widgetTypeService.saveWidgetType(widgetTypeDetails); throw new RuntimeException("Unable to parse widget type from json", e);
} catch (Exception e) { }
log.error("Unable to load widget type from json: [{}]", path.toString()); });
throw new RuntimeException("Unable to load widget type from json", e);
}
}
);
}
}
this.loadSystemScadaSymbols();
for (var widgetsBundleDescriptorEntry : widgetsBundlesMap.entrySet()) {
Path path = widgetsBundleDescriptorEntry.getKey();
try {
JsonNode widgetsBundleDescriptorJson = widgetsBundleDescriptorEntry.getValue();
JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
List<String> widgetTypeFqns = new ArrayList<>();
if (widgetsBundleDescriptorJson.has("widgetTypes")) {
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
widgetTypesArrayJson.forEach(
widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails);
widgetTypeFqns.add(savedWidgetType.getFqn());
} catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widget type from json", e);
}
}
);
}
if (widgetsBundleDescriptorJson.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptorJson.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, savedWidgetsBundle.getId(), widgetTypeFqns);
} catch (Exception e) {
log.error("Unable to load widgets bundle from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widgets bundle from json", e);
} }
} else {
widgets = Stream.empty();
} }
tbWidgetsBundleService.updateWidgets(TenantId.SYS_TENANT_ID, bundles, widgets);
loadSystemScadaSymbols();
} }
private void loadSystemScadaSymbols() throws Exception { private void loadSystemScadaSymbols() throws Exception {
@ -355,7 +297,7 @@ public class InstallScripts {
} }
private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol, private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol,
ImageUtils.ScadaSymbolMetadataInfo metadata) { ImageUtils.ScadaSymbolMetadataInfo metadata) {
String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink(); String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink();
WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails(); WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails();
JsonNode descriptor = JacksonUtil.clone(template.getDescriptor()); JsonNode descriptor = JacksonUtil.clone(template.getDescriptor());
@ -375,38 +317,22 @@ public class InstallScripts {
defaultConfig.put("title", metadata.getTitle()); defaultConfig.put("title", metadata.getTitle());
ObjectNode settings; ObjectNode settings;
if (defaultConfig.has("settings")) { if (defaultConfig.has("settings")) {
settings = (ObjectNode)defaultConfig.get("settings"); settings = (ObjectNode) defaultConfig.get("settings");
} else { } else {
settings = JacksonUtil.newObjectNode(); settings = JacksonUtil.newObjectNode();
defaultConfig.set("settings", settings); defaultConfig.set("settings", settings);
} }
settings.put("scadaSymbolUrl", symbolUrl); settings.put("scadaSymbolUrl", symbolUrl);
((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig)); ((ObjectNode) descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX()); ((ObjectNode) descriptor).put("sizeX", metadata.getWidgetSizeX());
((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY()); ((ObjectNode) descriptor).put("sizeY", metadata.getWidgetSizeY());
String controllerScript = descriptor.get("controllerScript").asText(); String controllerScript = descriptor.get("controllerScript").asText();
controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'"); controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'");
controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "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); 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());
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
}
}
public void updateImages() { public void updateImages() {
imagesUpdater.updateWidgetsBundlesImages(); imagesUpdater.updateWidgetsBundlesImages();
imagesUpdater.updateWidgetTypesImages(); imagesUpdater.updateWidgetTypesImages();
@ -419,7 +345,7 @@ public class InstallScripts {
public void loadSystemImages() { public void loadSystemImages() {
log.info("Loading system images..."); log.info("Loading system images...");
Stream<Path> dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)), 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) { try (dashboardsFiles) {
dashboardsFiles.forEach(file -> { dashboardsFiles.forEach(file -> {
try { try {
@ -534,4 +460,5 @@ public class InstallScripts {
resourceService.saveResource(resource); resourceService.saveResource(resource);
} }
} }
} }

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

@ -16,13 +16,13 @@
package org.thingsboard.server.service.sync; package org.thingsboard.server.service.sync;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; 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;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType; import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile; import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
@ -36,8 +36,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service @Service
@RequiredArgsConstructor
@Slf4j @Slf4j
public class DefaultGitSyncService implements GitSyncService { public class DefaultGitSyncService implements GitSyncService {
@ -69,11 +69,11 @@ public class DefaultGitSyncService implements GitSyncService {
} }
try { try {
log.debug("Fetching {} repository", key); log.debug("[{}] Fetching repository", key);
repository.fetch(); repository.fetch();
onUpdate(key); onUpdate(key);
} catch (Throwable e) { } catch (Throwable e) {
log.error("Failed to fetch {} repository", key, e); log.error("[{}] Failed to fetch repository", key, e);
} }
}, fetchFrequencyMs, fetchFrequencyMs, TimeUnit.MILLISECONDS); }, fetchFrequencyMs, fetchFrequencyMs, TimeUnit.MILLISECONDS);
} }
@ -93,7 +93,7 @@ public class DefaultGitSyncService implements GitSyncService {
try { try {
return repository.getFileContentAtCommit(path, getBranchRef(repository)); return repository.getFileContentAtCommit(path, getBranchRef(repository));
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to get file content for path {} for {} repository: {}", path, key, e.getMessage()); log.warn("[{}] Failed to get file content for path {}: {}", key, path, e.getMessage());
return "{}"; return "{}";
} }
} }
@ -118,7 +118,7 @@ public class DefaultGitSyncService implements GitSyncService {
repository = repositories.get(key); repository = repositories.get(key);
if (repository == null) { if (repository == null) {
throw new IllegalStateException("{} repository is not initialized"); throw new IllegalStateException(key + " repository is not initialized");
} }
return repository; return repository;
} }
@ -130,18 +130,23 @@ public class DefaultGitSyncService implements GitSyncService {
GitRepository repository = GitRepository.openOrClone(directory, settings, true); GitRepository repository = GitRepository.openOrClone(directory, settings, true);
repositories.put(key, repository); repositories.put(key, repository);
log.info("Initialized {} repository", key); log.info("[{}] Initialized repository", key);
onUpdate(key); onUpdate(key);
} catch (Throwable e) { } catch (Throwable e) {
log.error("Failed to init {} repository for settings {}", key, settings, e); log.error("[{}] Failed to initialize repository with settings {}", key, settings, e);
} }
} }
private void onUpdate(String key) { private void onUpdate(String key) {
Runnable listener = updateListeners.get(key); Runnable listener = updateListeners.get(key);
if (listener != null) { if (listener != null) {
listener.run(); log.debug("[{}] Handling repository update", key);
try {
listener.run();
} catch (Throwable e) {
log.error("[{}] Failed to handle repository update", key, e);
}
} }
} }

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

@ -1244,6 +1244,15 @@ transport:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
# Interval of transport statistics logging # Interval of transport statistics logging
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" 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}"
# Fetch frequency in hours for gateways dashboard repository
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
# CoAP server parameters # CoAP server parameters
coap: coap:

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

@ -0,0 +1,51 @@
/**
* 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.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
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();
});
}
}

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

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

2
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); TbResource findResourceById(TenantId tenantId, TbResourceId resourceId);
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);

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 deleteWidgetTypesByTenantId(TenantId tenantId);
void deleteWidgetTypesByBundleId(TenantId tenantId, WidgetsBundleId bundleId);
} }

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), JKS("application/x-java-keystore", false, false),
PKCS_12("application/x-pkcs12", false, false), PKCS_12("application/x-pkcs12", false, false),
JS_MODULE("application/javascript", true, true), JS_MODULE("application/javascript", true, true),
IMAGE(null, true, true); IMAGE(null, true, true),
DASHBOARD("application/json", true, true);
@Getter @Getter
private final String mediaType; 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; package org.thingsboard.server.common.data.util;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@FunctionalInterface @FunctionalInterface
public interface ThrowingSupplier<T> { public interface ThrowingSupplier<T> {
T get() throws Exception; T get() throws ThingsboardException;
} }

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

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

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

@ -120,6 +120,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceDao.findById(tenantId, resourceId.getId()); 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 @Override
public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
@ -256,4 +262,5 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId())); 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -218,7 +217,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
List<WidgetTypeId> toRemove = existingBundleWidgets.stream() List<WidgetTypeId> toRemove = existingBundleWidgets.stream()
.map(WidgetsBundleWidget::getWidgetTypeId) .map(WidgetsBundleWidget::getWidgetTypeId)
.filter(widgetTypeId -> bundleWidgets.stream().noneMatch(newBundleWidget -> .filter(widgetTypeId -> bundleWidgets.stream().noneMatch(newBundleWidget ->
newBundleWidget.getWidgetTypeId().equals(widgetTypeId))).collect(Collectors.toList()); newBundleWidget.getWidgetTypeId().equals(widgetTypeId))).toList();
for (WidgetTypeId widgetTypeId : toRemove) { for (WidgetTypeId widgetTypeId : toRemove) {
widgetTypeDao.removeWidgetTypeFromWidgetsBundle(widgetsBundleId.getId(), widgetTypeId.getId()); widgetTypeDao.removeWidgetTypeFromWidgetsBundle(widgetsBundleId.getId(), widgetTypeId.getId());
} }
@ -243,6 +242,12 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
tenantWidgetTypeRemover.removeEntities(tenantId, tenantId); 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 @Override
public void deleteByTenantId(TenantId tenantId) { public void deleteByTenantId(TenantId tenantId) {
deleteWidgetTypesByTenantId(tenantId); deleteWidgetTypesByTenantId(tenantId);
@ -258,24 +263,36 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return EntityType.WIDGET_TYPE; return EntityType.WIDGET_TYPE;
} }
private PaginatedRemover<TenantId, WidgetTypeInfo> tenantWidgetTypeRemover = private final PaginatedRemover<TenantId, WidgetTypeInfo> tenantWidgetTypeRemover = new PaginatedRemover<>() {
new PaginatedRemover<>() {
@Override
@Override protected PageData<WidgetTypeInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
protected PageData<WidgetTypeInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return widgetTypeDao.findTenantWidgetTypesByTenantId(
return widgetTypeDao.findTenantWidgetTypesByTenantId( WidgetTypeFilter.builder()
WidgetTypeFilter.builder() .tenantId(id)
.tenantId(id) .fullSearch(false)
.fullSearch(false) .deprecatedFilter(DeprecatedFilter.ALL)
.deprecatedFilter(DeprecatedFilter.ALL) .widgetTypes(null).build(),
.widgetTypes(null).build(), pageLink);
pageLink); }
}
@Override
@Override protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) {
protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) { deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId()));
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());
}
};
} }

Loading…
Cancel
Save