Browse Source

Reference resources by link in dashboards and widgets

pull/11873/head
ViacheslavKlimov 2 years ago
parent
commit
9f10217538
  1. 3
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  2. 35
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  3. 15
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  4. 65
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  5. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
  9. 69
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
  11. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java
  13. 110
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java
  14. 18
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java
  15. 64
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  16. 9
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  17. 3
      application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
  18. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
  19. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  20. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java
  21. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  22. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java
  23. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  24. 58
      application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
  25. 4
      application/src/test/resources/logback-test.xml
  26. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  27. 9
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  28. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  29. 6
      common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
  30. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java
  32. 7
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
  33. 28
      common/data/src/main/java/org/thingsboard/server/common/data/dashboard/DashboardExportData.java
  34. 27
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetExportData.java
  35. 7
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  36. 28
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsExportData.java
  37. 11
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  38. 19
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  39. 122
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  40. 11
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  41. 6
      ui-ngx/src/app/core/http/dashboard.service.ts
  42. 7
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  43. 6
      ui-ngx/src/app/shared/models/dashboard.models.ts
  44. 1
      ui-ngx/src/app/shared/models/widget.models.ts

3
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -34,6 +34,9 @@ public class ControllerConstants {
"See the 'Model' tab of the Response Class for more details. "; "See the 'Model' tab of the Response Class for more details. ";
protected static final String INLINE_IMAGES = "inlineImages"; protected static final String INLINE_IMAGES = "inlineImages";
protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)";
protected static final String INCLUDE_RESOURCES = "includeResources";
protected static final String INCLUDE_RESOURCES_DESCRIPTION = "Export used resources and replace resource links with resource metadata";
protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";

35
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -45,7 +45,6 @@ import org.thingsboard.server.common.data.HomeDashboard;
import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.dashboard.DashboardExportData;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -55,9 +54,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
@ -78,6 +77,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.INCLUDE_RESOURCES;
import static org.thingsboard.server.controller.ControllerConstants.INCLUDE_RESOURCES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -97,7 +98,8 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
public class DashboardController extends BaseController { public class DashboardController extends BaseController {
private final TbDashboardService tbDashboardService; private final TbDashboardService tbDashboardService;
private final ImageService imageService; private final TbResourceService tbResourceService;
public static final String DASHBOARD_ID = "dashboardId"; public static final String DASHBOARD_ID = "dashboardId";
private static final String HOME_DASHBOARD_ID = "homeDashboardId"; private static final String HOME_DASHBOARD_ID = "homeDashboardId";
@ -149,19 +151,16 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/{dashboardId}") @GetMapping(value = "/dashboard/{dashboardId}")
public Dashboard getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) public Dashboard getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @PathVariable(DASHBOARD_ID) String strDashboardId,
@Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
@RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources) throws ThingsboardException {
checkParameter(DASHBOARD_ID, strDashboardId); checkParameter(DASHBOARD_ID, strDashboardId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
return checkDashboardId(dashboardId, Operation.READ);
}
@GetMapping(value = "/dashboard/{dashboardId}/export")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
public DashboardExportData exportDashboard(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) UUID id) throws ThingsboardException {
DashboardId dashboardId = new DashboardId(id);
Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
return tbDashboardService.exportDashboard(getTenantId(), dashboard, getCurrentUser()); if (includeResources) {
dashboard.setResources(tbResourceService.exportResources(dashboard, getCurrentUser()));
}
return dashboard;
} }
@ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)",
@ -180,16 +179,6 @@ public class DashboardController extends BaseController {
return tbDashboardService.save(dashboard, getCurrentUser()); return tbDashboardService.save(dashboard, getCurrentUser());
} }
@PostMapping(value = "/dashboard/import")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
public Dashboard importDashboard(@RequestBody DashboardExportData exportData) throws Exception {
Dashboard dashboard = exportData.getDashboard();
dashboard.setTenantId(getTenantId());
dashboard.setId(null);
checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
return tbDashboardService.importDashboard(exportData, getCurrentUser());
}
@ApiOperation(value = "Delete the Dashboard (deleteDashboard)", @ApiOperation(value = "Delete the Dashboard (deleteDashboard)",
notes = "Delete the Dashboard." + TENANT_AUTHORITY_PARAGRAPH) notes = "Delete the Dashboard." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")

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

@ -173,6 +173,21 @@ public class TbResourceController extends BaseController {
return checkResourceInfoId(resourceId, Operation.READ); return checkResourceInfoId(resourceId, Operation.READ);
} }
@ApiOperation(value = "Get resource info (getResourceInfo)",
notes = "Get info for the resource with the given type, scope and key. " +
RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource/{resourceType}/{scope}/{key}/info")
public TbResourceInfo getResourceInfo(@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) throws ThingsboardException {
ResourceType resourceType = ResourceType.valueOf(resourceTypeStr.toUpperCase());
return checkResourceInfo(scope, resourceType, key, Operation.READ);
}
@ApiOperation(value = "Get Resource (getResourceById)", @ApiOperation(value = "Get Resource (getResourceById)",
notes = "Fetch the Resource object based on the provided Resource Id. " + notes = "Fetch the Resource object based on the provided Resource Id. " +
RESOURCE_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, hidden = true) RESOURCE_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, hidden = true)

65
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -23,7 +23,6 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -41,26 +40,27 @@ 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.widget.DeprecatedFilter; import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetExportData;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsExportData;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.INCLUDE_RESOURCES;
import static org.thingsboard.server.controller.ControllerConstants.INCLUDE_RESOURCES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -78,6 +78,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_
public class WidgetTypeController extends AutoCommitController { public class WidgetTypeController extends AutoCommitController {
private final TbWidgetTypeService tbWidgetTypeService; private final TbWidgetTypeService tbWidgetTypeService;
private final TbResourceService tbResourceService;
private static final String WIDGET_TYPE_DESCRIPTION = "Widget Type represents the template for widget creation. Widget Type and Widget are similar to class and object in OOP theory."; private static final String WIDGET_TYPE_DESCRIPTION = "Widget Type represents the template for widget creation. Widget Type and Widget are similar to class and object in OOP theory.";
private static final String WIDGET_TYPE_DETAILS_DESCRIPTION = "Widget Type Details extend Widget Type and add image and description properties. " + private static final String WIDGET_TYPE_DETAILS_DESCRIPTION = "Widget Type Details extend Widget Type and add image and description properties. " +
@ -95,18 +96,16 @@ public class WidgetTypeController extends AutoCommitController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetType/{widgetTypeId}") @GetMapping(value = "/widgetType/{widgetTypeId}")
public WidgetTypeDetails getWidgetTypeById(@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) public WidgetTypeDetails getWidgetTypeById(@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { @PathVariable("widgetTypeId") String strWidgetTypeId,
@Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
@RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources) throws ThingsboardException {
checkParameter("widgetTypeId", strWidgetTypeId); checkParameter("widgetTypeId", strWidgetTypeId);
WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
return checkWidgetTypeId(widgetTypeId, Operation.READ); WidgetTypeDetails widgetTypeDetails = checkWidgetTypeId(widgetTypeId, Operation.READ);
} if (includeResources) {
widgetTypeDetails.setResources(tbResourceService.exportResources(widgetTypeDetails, getCurrentUser()));
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") }
@GetMapping(value = "/widgetType/{widgetTypeId}/export") return widgetTypeDetails;
public WidgetExportData exportWidgetType(@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") UUID widgetTypeId) throws ThingsboardException {
WidgetTypeDetails widgetTypeDetails = checkWidgetTypeId(new WidgetTypeId(widgetTypeId), Operation.READ);
return tbWidgetTypeService.exportWidgetType(getTenantId(), widgetTypeDetails, getCurrentUser());
} }
@ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)", @ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)",
@ -151,15 +150,6 @@ public class WidgetTypeController extends AutoCommitController {
return tbWidgetTypeService.save(widgetTypeDetails, updateExistingByFqn != null && updateExistingByFqn, currentUser); return tbWidgetTypeService.save(widgetTypeDetails, updateExistingByFqn != null && updateExistingByFqn, currentUser);
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/widgetType/import")
public WidgetTypeDetails importWidgetType(@RequestBody WidgetExportData exportData) throws Exception {
WidgetTypeDetails widgetTypeDetails = exportData.getWidgetTypeDetails();
widgetTypeDetails.setTenantId(getTenantId());
checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE);
return tbWidgetTypeService.importWidgetType(exportData, getCurrentUser());
}
@ApiOperation(value = "Delete widget type (deleteWidgetType)", @ApiOperation(value = "Delete widget type (deleteWidgetType)",
notes = "Deletes the Widget Type. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Deletes the Widget Type. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -283,26 +273,19 @@ public class WidgetTypeController extends AutoCommitController {
@ResponseBody @ResponseBody
public List<WidgetTypeDetails> getBundleWidgetTypesDetails( public List<WidgetTypeDetails> getBundleWidgetTypesDetails(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId @RequestParam("widgetsBundleId") String strWidgetsBundleId,
@Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
@RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources
) throws ThingsboardException { ) throws ThingsboardException {
SecurityUser user = getCurrentUser();
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); List<WidgetTypeDetails> result = checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId));
} if (includeResources) {
for (WidgetTypeDetails widgetTypeDetails : result) {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") widgetTypeDetails.setResources(tbResourceService.exportResources(widgetTypeDetails, user));
@GetMapping(value = "/widgetTypes/export", params = {"widgetsBundleId"}) }
public WidgetsExportData exportBundleWidgetTypes(@Parameter(description = "Widget Bundle Id", required = true) }
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { return result;
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return tbWidgetTypeService.exportBundleWidgetTypes(getTenantId(), widgetsBundleId, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/widgetTypes/import", params = {"widgetsBundleId"})
public List<WidgetTypeDetails> importBundleWidgetTypes(@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
// FIXME: determine how bundle widget types were imported before
throw new UnsupportedOperationException();
} }
@ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)",

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

@ -145,6 +145,7 @@ public class ThingsboardInstallService {
case "3.8.1": case "3.8.1":
log.info("Upgrading ThingsBoard from version 3.8.1 to 3.9.0 ..."); log.info("Upgrading ThingsBoard from version 3.8.1 to 3.9.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.8.1"); databaseEntitiesUpgradeService.upgradeDatabase("3.8.1");
dataUpdateService.updateData("3.8.1");
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break; break;
default: default:

3
application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy; package org.thingsboard.server.service.entitiy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface SimpleTbEntityService<T> { public interface SimpleTbEntityService<T> {
@ -23,7 +24,7 @@ public interface SimpleTbEntityService<T> {
return save(entity, null); return save(entity, null);
} }
T save(T entity, User user) throws Exception; T save(T entity, SecurityUser user) throws Exception;
void delete(T entity, User user); void delete(T entity, User user);

3
application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
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 org.thingsboard.server.service.security.model.SecurityUser;
@Service @Service
@TbCoreComponent @TbCoreComponent
@ -38,7 +39,7 @@ public class DefaultTbAssetProfileService extends AbstractTbEntityService implem
private final AssetProfileService assetProfileService; private final AssetProfileService assetProfileService;
@Override @Override
public AssetProfile save(AssetProfile assetProfile, User user) throws Exception { public AssetProfile save(AssetProfile assetProfile, SecurityUser user) throws Exception {
ActionType actionType = assetProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = assetProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = assetProfile.getTenantId(); TenantId tenantId = assetProfile.getTenantId();
try { try {

3
application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java

@ -24,13 +24,14 @@ import org.thingsboard.server.common.data.audit.ActionType;
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.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
public class DefaultTbCustomerService extends AbstractTbEntityService implements TbCustomerService { public class DefaultTbCustomerService extends AbstractTbEntityService implements TbCustomerService {
@Override @Override
public Customer save(Customer customer, User user) throws Exception { public Customer save(Customer customer, SecurityUser user) throws Exception {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId(); TenantId tenantId = customer.getTenantId();
try { try {

69
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java

@ -16,38 +16,27 @@
package org.thingsboard.server.service.entitiy.dashboard; package org.thingsboard.server.service.entitiy.dashboard;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.TbResourceInfo;
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.dashboard.DashboardExportData;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
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 org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
@Service @Service
@ -56,15 +45,17 @@ import java.util.Set;
public class DefaultTbDashboardService extends AbstractTbEntityService implements TbDashboardService { public class DefaultTbDashboardService extends AbstractTbEntityService implements TbDashboardService {
private final DashboardService dashboardService; private final DashboardService dashboardService;
private final ImageService imageService;
private final TbImageService tbImageService;
private final ResourceService resourceService;
private final TbResourceService tbResourceService; private final TbResourceService tbResourceService;
@Override @Override
public Dashboard save(Dashboard dashboard, User user) throws Exception { public Dashboard save(Dashboard dashboard, SecurityUser user) throws Exception {
ActionType actionType = dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = dashboard.getTenantId(); TenantId tenantId = dashboard.getTenantId();
if (CollectionUtils.isNotEmpty(dashboard.getResources())) {
tbResourceService.importResources(dashboard.getResources(), user);
}
try { try {
Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard)); Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
autoCommit(user, savedDashboard.getId()); autoCommit(user, savedDashboard.getId());
@ -301,50 +292,4 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
} }
} }
@Override
public DashboardExportData exportDashboard(TenantId tenantId, Dashboard dashboard, SecurityUser user) throws ThingsboardException {
Map<TbResourceId, TbResourceInfo> resources = new HashMap<>();
for (TbResourceInfo imageInfo : imageService.inlineImages(dashboard)) {
resources.putIfAbsent(imageInfo.getId(), imageInfo);
}
for (TbResourceInfo resourceInfo : resourceService.processResourcesForExport(dashboard)) {
resources.putIfAbsent(resourceInfo.getId(), resourceInfo);
}
for (TbResourceInfo resourceInfo : resources.values()) {
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo);
}
DashboardExportData exportData = new DashboardExportData();
exportData.setDashboard(dashboard);
exportData.setResources(resources.values().stream()
.map(resourceInfo -> {
if (resourceInfo.getResourceType() == ResourceType.IMAGE) {
ResourceExportData imageExportData = tbImageService.exportImage(resourceInfo);
imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import
return imageExportData;
} else {
return tbResourceService.exportResource(resourceInfo);
}
})
.toList());
return exportData;
}
@Override
public Dashboard importDashboard(DashboardExportData exportData, SecurityUser user) throws Exception {
Map<TbResourceId, TbResourceId> importedResources = new HashMap<>();
for (ResourceExportData resourceExportData : exportData.getResources()) {
if (resourceExportData.getType() == ResourceType.IMAGE) {
tbImageService.importImage(resourceExportData, true, user);
} else {
TbResourceInfo importedResource = tbResourceService.importResource(resourceExportData, true, user);
importedResources.put(resourceExportData.getId(), importedResource.getId());
}
}
Dashboard dashboard = exportData.getDashboard();
resourceService.processResourcesForImport(dashboard, importedResources);
return save(dashboard, user);
}
} }

6
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java

@ -18,14 +18,12 @@ package org.thingsboard.server.service.entitiy.dashboard;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.dashboard.DashboardExportData;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Set; import java.util.Set;
@ -49,8 +47,4 @@ public interface TbDashboardService extends SimpleTbEntityService<Dashboard> {
Dashboard unassignDashboardFromCustomer(Dashboard dashboard, Customer customer, User user) throws ThingsboardException; Dashboard unassignDashboardFromCustomer(Dashboard dashboard, Customer customer, User user) throws ThingsboardException;
DashboardExportData exportDashboard(TenantId tenantId, Dashboard dashboard, SecurityUser user) throws ThingsboardException;
Dashboard importDashboard(DashboardExportData exportData, SecurityUser user) throws Exception;
} }

3
application/src/main/java/org/thingsboard/server/service/entitiy/device/profile/DefaultTbDeviceProfileService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
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 org.thingsboard.server.service.security.model.SecurityUser;
@Service @Service
@TbCoreComponent @TbCoreComponent
@ -38,7 +39,7 @@ public class DefaultTbDeviceProfileService extends AbstractTbEntityService imple
private final DeviceProfileService deviceProfileService; private final DeviceProfileService deviceProfileService;
@Override @Override
public DeviceProfile save(DeviceProfile deviceProfile, User user) throws Exception { public DeviceProfile save(DeviceProfile deviceProfile, SecurityUser user) throws Exception {
ActionType actionType = deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = deviceProfile.getTenantId(); TenantId tenantId = deviceProfile.getTenantId();
try { try {

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

@ -29,6 +29,7 @@ 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 org.thingsboard.server.service.security.model.SecurityUser;
import java.util.List; import java.util.List;
@ -42,7 +43,7 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme
private final WidgetTypeService widgetTypeService; private final WidgetTypeService widgetTypeService;
@Override @Override
public WidgetsBundle save(WidgetsBundle widgetsBundle, User user) throws Exception { public WidgetsBundle save(WidgetsBundle widgetsBundle, SecurityUser user) throws Exception {
ActionType actionType = widgetsBundle.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = widgetsBundle.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = widgetsBundle.getTenantId(); TenantId tenantId = widgetsBundle.getTenantId();
try { try {

110
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java

@ -16,57 +16,36 @@
package org.thingsboard.server.service.entitiy.widgets.type; package org.thingsboard.server.service.entitiy.widgets.type;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResourceInfo;
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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.widget.WidgetExportData;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsExportData;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
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 org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service @Service
@TbCoreComponent @TbCoreComponent
@AllArgsConstructor @AllArgsConstructor
public class DefaultWidgetTypeService extends AbstractTbEntityService implements TbWidgetTypeService { public class DefaultWidgetTypeService extends AbstractTbEntityService implements TbWidgetTypeService {
private final WidgetTypeService widgetTypeService; private final WidgetTypeService widgetTypeService;
private final ImageService imageService;
private final TbImageService tbImageService;
private final ResourceService resourceService;
private final TbResourceService tbResourceService; private final TbResourceService tbResourceService;
@Override @Override
public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception { public WidgetTypeDetails save(WidgetTypeDetails entity, SecurityUser user) throws Exception {
return this.save(entity, false, user); return this.save(entity, false, user);
} }
@Override @Override
public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception { public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, SecurityUser user) throws Exception {
TenantId tenantId = widgetTypeDetails.getTenantId(); TenantId tenantId = widgetTypeDetails.getTenantId();
if (widgetTypeDetails.getId() == null && StringUtils.isNotEmpty(widgetTypeDetails.getFqn()) && updateExistingByFqn) { if (widgetTypeDetails.getId() == null && StringUtils.isNotEmpty(widgetTypeDetails.getFqn()) && updateExistingByFqn) {
WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, widgetTypeDetails.getFqn()); WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, widgetTypeDetails.getFqn());
@ -74,6 +53,10 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
widgetTypeDetails.setId(widgetType.getId()); widgetTypeDetails.setId(widgetType.getId());
} }
} }
if (CollectionUtils.isNotEmpty(widgetTypeDetails.getResources())) {
tbResourceService.importResources(widgetTypeDetails.getResources(), user);
}
ActionType actionType = widgetTypeDetails.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = widgetTypeDetails.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try { try {
WidgetTypeDetails savedWidgetTypeDetails = checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails)); WidgetTypeDetails savedWidgetTypeDetails = checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails));
@ -100,83 +83,4 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
} }
} }
@Override
public WidgetExportData exportWidgetType(TenantId tenantId, WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException {
List<ResourceExportData> resources = processWidgetTypesForExport(List.of(widgetTypeDetails), user);
WidgetExportData exportData = new WidgetExportData();
exportData.setWidgetTypeDetails(widgetTypeDetails);
exportData.setResources(resources);
return exportData;
}
@Override
public WidgetsExportData exportBundleWidgetTypes(TenantId tenantId, WidgetsBundleId bundleId, SecurityUser user) throws ThingsboardException {
List<WidgetTypeDetails> widgetTypes = widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(tenantId, bundleId);
List<ResourceExportData> resources = processWidgetTypesForExport(widgetTypes, user);
WidgetsExportData exportData = new WidgetsExportData();
exportData.setWidgetTypesDetails(widgetTypes);
exportData.setResources(resources);
return exportData;
}
private List<ResourceExportData> processWidgetTypesForExport(List<WidgetTypeDetails> widgetTypes, SecurityUser user) throws ThingsboardException {
Map<TbResourceId, TbResourceInfo> resources = new HashMap<>();
for (WidgetTypeDetails widgetTypeDetails : widgetTypes) {
for (TbResourceInfo imageInfo : imageService.inlineImages(widgetTypeDetails)) {
resources.putIfAbsent(imageInfo.getId(), imageInfo);
}
for (TbResourceInfo resourceInfo : resourceService.processResourcesForExport(widgetTypeDetails)) {
resources.putIfAbsent(resourceInfo.getId(), resourceInfo);
}
}
for (TbResourceInfo resourceInfo : resources.values()) {
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo);
}
return resources.values().stream()
.map(resourceInfo -> {
if (resourceInfo.getResourceType() == ResourceType.IMAGE) {
ResourceExportData imageExportData = tbImageService.exportImage(resourceInfo);
imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import
return imageExportData;
} else {
return tbResourceService.exportResource(resourceInfo);
}
})
.toList();
}
@Override
public WidgetTypeDetails importWidgetType(WidgetExportData exportData, SecurityUser user) throws Exception {
return importWidgetTypes(List.of(exportData.getWidgetTypeDetails()), exportData.getResources(), user).get(0);
}
@Override
public List<WidgetTypeDetails> importWidgetTypes(WidgetsExportData exportData, SecurityUser user) throws Exception {
return importWidgetTypes(exportData.getWidgetTypesDetails(), exportData.getResources(), user);
}
private List<WidgetTypeDetails> importWidgetTypes(List<WidgetTypeDetails> widgetTypesDetails, List<ResourceExportData> resources, SecurityUser user) throws Exception {
Map<TbResourceId, TbResourceId> importedResources = new HashMap<>();
for (ResourceExportData resourceExportData : resources) {
if (resourceExportData.getType() == ResourceType.IMAGE) {
tbImageService.importImage(resourceExportData, true, user);
} else {
TbResourceInfo importedResource = tbResourceService.importResource(resourceExportData, true, user);
importedResources.put(resourceExportData.getId(), importedResource.getId());
}
}
List<WidgetTypeDetails> result = new ArrayList<>();
for (WidgetTypeDetails widgetTypeDetails : widgetTypesDetails) {
resourceService.processResourcesForImport(widgetTypeDetails, importedResources);
// FIXME: shouldn't just save it, have to be processed somehow
widgetTypeDetails = save(widgetTypeDetails, user);
result.add(widgetTypeDetails);
}
return result;
}
} }

18
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java

@ -15,28 +15,12 @@
*/ */
package org.thingsboard.server.service.entitiy.widgets.type; package org.thingsboard.server.service.entitiy.widgets.type;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.widget.WidgetExportData;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsExportData;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.List;
public interface TbWidgetTypeService extends SimpleTbEntityService<WidgetTypeDetails> { public interface TbWidgetTypeService extends SimpleTbEntityService<WidgetTypeDetails> {
WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception; WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, SecurityUser user) throws Exception;
WidgetExportData exportWidgetType(TenantId tenantId, WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException;
WidgetsExportData exportBundleWidgetTypes(TenantId tenantId, WidgetsBundleId bundleId, SecurityUser user) throws ThingsboardException;
WidgetTypeDetails importWidgetType(WidgetExportData exportData, SecurityUser user) throws Exception;
List<WidgetTypeDetails> importWidgetTypes(WidgetsExportData exportData, SecurityUser user) throws Exception;
} }

64
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.resource;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceSubType;
@ -32,6 +33,8 @@ 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.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
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;
@ -41,7 +44,10 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.Base64; import java.util.Base64;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -58,9 +64,11 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource
public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService { public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService {
private final ResourceService resourceService; private final ResourceService resourceService;
private final ImageService imageService;
private final TbImageService tbImageService;
@Override @Override
public TbResource save(TbResource resource, User user) throws ThingsboardException { public TbResource save(TbResource resource, SecurityUser user) throws ThingsboardException {
if (resource.getResourceType() == ResourceType.IMAGE) { if (resource.getResourceType() == ResourceType.IMAGE) {
throw new IllegalArgumentException("Image resource type is not supported"); throw new IllegalArgumentException("Image resource type is not supported");
} }
@ -124,10 +132,57 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
} }
@Override @Override
public ResourceExportData exportResource(TbResourceInfo resourceInfo) { public List<ResourceExportData> exportResources(Dashboard dashboard, SecurityUser user) throws ThingsboardException {
return exportResources(dashboard, imageService::inlineImages, resourceService::replaceResourcesUrlsWithTags, user);
}
@Override
public List<ResourceExportData> exportResources(WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException {
return exportResources(widgetTypeDetails, imageService::inlineImages, resourceService::replaceResourcesUrlsWithTags, user);
}
@Override
public void importResources(List<ResourceExportData> resources, SecurityUser user) throws Exception {
for (ResourceExportData resourceExportData : resources) {
if (resourceExportData.getType() == ResourceType.IMAGE) {
tbImageService.importImage(resourceExportData, true, user);
} else {
importResource(resourceExportData, true, user);
}
}
}
private <T> List<ResourceExportData> exportResources(T entity,
Function<T, List<TbResourceInfo>> imagesProcessor,
Function<T, List<TbResourceInfo>> resourcesProcessor,
SecurityUser user) throws ThingsboardException {
Map<TbResourceId, TbResourceInfo> resources = new HashMap<>();
for (TbResourceInfo imageInfo : imagesProcessor.apply(entity)) {
resources.putIfAbsent(imageInfo.getId(), imageInfo);
}
for (TbResourceInfo resourceInfo : resourcesProcessor.apply(entity)) {
resources.putIfAbsent(resourceInfo.getId(), resourceInfo);
}
for (TbResourceInfo resourceInfo : resources.values()) {
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo);
}
return resources.values().stream()
.map(resourceInfo -> {
if (resourceInfo.getResourceType() == ResourceType.IMAGE) {
ResourceExportData imageExportData = tbImageService.exportImage(resourceInfo);
imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import
return imageExportData;
} else {
return exportResource(resourceInfo);
}
})
.toList();
}
private ResourceExportData exportResource(TbResourceInfo resourceInfo) {
byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId()); byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
return ResourceExportData.builder() return ResourceExportData.builder()
.id(resourceInfo.getId())
.mediaType(resourceInfo.getResourceType().getMediaType()) .mediaType(resourceInfo.getResourceType().getMediaType())
.fileName(resourceInfo.getFileName()) .fileName(resourceInfo.getFileName())
.title(resourceInfo.getTitle()) .title(resourceInfo.getTitle())
@ -138,8 +193,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
.build(); .build();
} }
@Override private TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException {
public TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException {
if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) { || exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
throw new IllegalArgumentException("Image import not supported"); throw new IllegalArgumentException("Image import not supported");

9
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -15,13 +15,14 @@
*/ */
package org.thingsboard.server.service.resource; package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; 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.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -39,8 +40,10 @@ public interface TbResourceService extends SimpleTbEntityService<TbResource> {
String sortOrder, String sortOrder,
PageLink pageLink); PageLink pageLink);
ResourceExportData exportResource(TbResourceInfo resourceInfo); List<ResourceExportData> exportResources(Dashboard dashboard, SecurityUser user) throws ThingsboardException;
TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException; List<ResourceExportData> exportResources(WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException;
void importResources(List<ResourceExportData> resources, SecurityUser user) throws Exception;
} }

3
application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java

@ -47,6 +47,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.utils.TbNodeUpgradeUtils; import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -170,7 +171,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
} }
@Override @Override
public RuleChain save(RuleChain ruleChain, User user) throws Exception { public RuleChain save(RuleChain ruleChain, SecurityUser user) throws Exception {
ActionType actionType = ruleChain.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = ruleChain.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = ruleChain.getTenantId(); TenantId tenantId = ruleChain.getTenantId();
try { try {

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java

@ -49,6 +49,7 @@ public class DashboardExportService extends BaseEntityExportService<DashboardId,
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN); replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN);
} }
imageService.inlineImages(dashboard); imageService.inlineImages(dashboard);
resourceService.replaceResourcesUrlsWithTags(dashboard);
} }
@Override @Override

3
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService; import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
@ -62,6 +63,8 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
private AttributesService attributesService; private AttributesService attributesService;
@Autowired @Autowired
protected ImageService imageService; protected ImageService imageService;
@Autowired
protected ResourceService resourceService;
@Override @Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException { public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java

@ -37,6 +37,7 @@ public class WidgetTypeExportService extends BaseEntityExportService<WidgetTypeI
throw new IllegalArgumentException("Export of system Widget Type is not allowed"); throw new IllegalArgumentException("Export of system Widget Type is not allowed");
} }
imageService.inlineImages(widgetTypeDetails); imageService.inlineImages(widgetTypeDetails);
resourceService.replaceResourcesUrlsWithTags(widgetTypeDetails);
} }
@Override @Override

1
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -49,7 +49,6 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
private final DashboardService dashboardService; private final DashboardService dashboardService;
@Override @Override
protected void setOwner(TenantId tenantId, Dashboard dashboard, IdProvider idProvider) { protected void setOwner(TenantId tenantId, Dashboard dashboard, IdProvider idProvider) {
dashboard.setTenantId(tenantId); dashboard.setTenantId(tenantId);

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java

@ -39,8 +39,8 @@ public class WidgetTypeImportService extends BaseEntityImportService<WidgetTypeI
} }
@Override @Override
protected WidgetTypeDetails prepare(EntitiesImportCtx ctx, WidgetTypeDetails widgetsBundle, WidgetTypeDetails old, WidgetTypeExportData exportData, IdProvider idProvider) { protected WidgetTypeDetails prepare(EntitiesImportCtx ctx, WidgetTypeDetails widgetTypeDetails, WidgetTypeDetails old, WidgetTypeExportData exportData, IdProvider idProvider) {
return widgetsBundle; return widgetTypeDetails;
} }
@Override @Override

12
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -1173,11 +1173,17 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
} }
protected <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception { protected <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception {
return uploadResource(httpMethod, url, filename, mediaType, content, StringUtils.isNotEmpty(subType) ?
List.of(new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8))) : null);
}
protected <R> TbResourceInfo uploadResource(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content, List<MockPart> otherParts) throws Exception {
MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content); MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content);
var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file); var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file);
if (StringUtils.isNotEmpty(subType)) { if (otherParts != null && !otherParts.isEmpty()) {
var imageSubTypePart = new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8)); for (MockPart otherPart : otherParts) {
request.part(imageSubTypePart); request.part(otherPart);
}
} }
setJwtToken(request); setJwtToken(request);
return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class); return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class);

58
application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java

@ -34,14 +34,15 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.dashboard.DashboardExportData;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
@ -55,6 +56,8 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -583,34 +586,67 @@ public class DashboardControllerTest extends AbstractControllerTest {
} }
@Test @Test
public void testExportImportDashboard() throws Exception { public void testExportImportDashboardWithResources() throws Exception {
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", "image12.png", "image/png", ImageControllerTest.PNG_IMAGE); TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", "image12.png", "image/png", ImageControllerTest.PNG_IMAGE);
TbResource resource = new TbResource();
resource.setResourceKey("gateway-management-extension.js");
resource.setFileName(resource.getResourceKey());
resource.setTitle(resource.getResourceKey());
resource.setResourceType(ResourceType.JS_MODULE);
byte[] resourceData = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
resource.setData(resourceData);
TbResourceInfo resourceInfo = doPost("/api/resource", resource, TbResourceInfo.class);
assertThat(resourceInfo.getLink()).isEqualTo("/api/resource/js_module/tenant/gateway-management-extension.js");
Dashboard dashboard = new Dashboard(); Dashboard dashboard = new Dashboard();
dashboard.setTitle("My dashboard"); dashboard.setTitle("My dashboard");
dashboard.setConfiguration(JacksonUtil.newObjectNode() dashboard.setConfiguration(JacksonUtil.newObjectNode()
.put("someImage", "tb-image;/api/images/tenant/" + imageInfo.getResourceKey())); .put("someImage", "tb-image;/api/images/tenant/" + imageInfo.getResourceKey())
.set("widgets", JacksonUtil.toJsonNode("""
{"xxx":
{"config":{"actions":{"elementClick":[
{"customResources":[{"url":{"entityType":"TB_RESOURCE","id":
"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}]}]}}}}
""")));
dashboard = doPost("/api/dashboard", dashboard, Dashboard.class); dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
DashboardExportData dashboardExportData = doGet("/api/dashboard/" + dashboard.getUuidId() + "/export", DashboardExportData.class); Dashboard exportedDashboard = doGet("/api/dashboard/" + dashboard.getUuidId() + "?includeResources=true", Dashboard.class);
String imageRef = dashboardExportData.getDashboard().getConfiguration().get("someImage").asText(); exportedDashboard.setId(null);
String imageRef = exportedDashboard.getConfiguration().get("someImage").asText();
assertThat(imageRef).isEqualTo("tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes()) + ":" assertThat(imageRef).isEqualTo("tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes()) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getName().getBytes()) + ":" + Base64.getEncoder().encodeToString(imageInfo.getName().getBytes()) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes()) + ":" + Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes()) + ":"
+ imageInfo.getEtag() + ";data:image/png;base64,"); + imageInfo.getEtag() + ";data:image/png;base64,");
String resourceRef = exportedDashboard.getConfiguration().get("widgets").get("xxx").get("config")
List<ResourceExportData> resources = dashboardExportData.getResources(); .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").get("id").asText();
assertThat(resources).singleElement().satisfies(resource -> { assertThat(resourceRef).isEqualTo("tb-resource:" + Base64.getEncoder().encodeToString(resourceInfo.getResourceType().name().getBytes())
assertThat(resource.getResourceKey()).isEqualTo(imageInfo.getResourceKey()); + ":" + resourceInfo.getEtag());
assertThat(resource.getData()).isEqualTo(Base64.getEncoder().encodeToString(ImageControllerTest.PNG_IMAGE));
Map<ResourceType, List<ResourceExportData>> resources = exportedDashboard.getResources().stream()
.collect(Collectors.groupingBy(ResourceExportData::getType));
assertThat(resources.get(ResourceType.IMAGE)).singleElement().satisfies(exportedImage -> {
assertThat(exportedImage.getFileName()).isEqualTo(imageInfo.getResourceKey());
assertThat(exportedImage.getData()).isEqualTo(Base64.getEncoder().encodeToString(ImageControllerTest.PNG_IMAGE));
});
assertThat(resources.get(ResourceType.JS_MODULE)).singleElement().satisfies(exportedJsModule -> {
assertThat(exportedJsModule.getFileName()).isEqualTo(resource.getResourceKey());
assertThat(exportedJsModule.getData()).isEqualTo(Base64.getEncoder().encodeToString(resourceData));
}); });
doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk()); doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk());
doDelete("/api/images/tenant/" + imageInfo.getResourceKey()).andExpect(status().isOk()); doDelete("/api/images/tenant/" + imageInfo.getResourceKey()).andExpect(status().isOk());
doDelete("/api/resource/" + resourceInfo.getId()).andExpect(status().isOk());
Dashboard importedDashboard = doPost("/api/dashboard/import", dashboardExportData, Dashboard.class); Dashboard importedDashboard = doPost("/api/dashboard", exportedDashboard, Dashboard.class);
assertThat(importedDashboard.getConfiguration().get("someImage").asText()).isEqualTo("tb-image;/api/images/tenant/" + imageInfo.getResourceKey()); assertThat(importedDashboard.getConfiguration().get("someImage").asText()).isEqualTo("tb-image;/api/images/tenant/" + imageInfo.getResourceKey());
TbResourceInfo importedImageInfo = doGet("/api/images/tenant/" + imageInfo.getResourceKey() + "/info", TbResourceInfo.class); TbResourceInfo importedImageInfo = doGet("/api/images/tenant/" + imageInfo.getResourceKey() + "/info", TbResourceInfo.class);
assertThat(importedImageInfo.getEtag()).isEqualTo(imageInfo.getEtag()); assertThat(importedImageInfo.getEtag()).isEqualTo(imageInfo.getEtag());
assertThat(importedImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey());
TbResourceInfo importedResourceInfo = doGet(resourceInfo.getLink() + "/info", TbResourceInfo.class);
assertThat(importedResourceInfo.getEtag()).isEqualTo(resourceInfo.getEtag());
assertThat(importedResourceInfo.getResourceKey()).isEqualTo(resourceInfo.getResourceKey());
} }
private Dashboard createDashboard(String title) { private Dashboard createDashboard(String title) {

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

@ -17,9 +17,7 @@
<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/dashboard/DashboardService.java

@ -78,4 +78,6 @@ public interface DashboardService extends EntityDaoService {
boolean existsById(TenantId tenantId, DashboardId dashboardId); boolean existsById(TenantId tenantId, DashboardId dashboardId);
PageData<DashboardId> findAllDashboardsIds(PageLink pageLink);
} }

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

@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List; import java.util.List;
import java.util.Map;
public interface ResourceService extends EntityDaoService { public interface ResourceService extends EntityDaoService {
@ -71,13 +70,13 @@ public interface ResourceService extends EntityDaoService {
TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag); TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag);
List<TbResourceInfo> processResourcesForExport(Dashboard dashboard); boolean replaceResourcesUsageWithUrls(Dashboard dashboard);
List<TbResourceInfo> processResourcesForExport(WidgetTypeDetails widgetTypeDetails); boolean replaceResourcesUsageWithUrls(WidgetTypeDetails widgetTypeDetails);
void processResourcesForImport(Dashboard dashboard, Map<TbResourceId, TbResourceId> importedResources); List<TbResourceInfo> replaceResourcesUrlsWithTags(Dashboard dashboard);
void processResourcesForImport(WidgetTypeDetails widgetTypeDetails, Map<TbResourceId, TbResourceId> importedResources); List<TbResourceInfo> replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails);
TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, String data); TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, String data);

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

@ -67,4 +67,6 @@ public interface WidgetTypeService extends EntityDaoService {
void deleteWidgetTypesByBundleId(TenantId tenantId, WidgetsBundleId bundleId); void deleteWidgetTypesByBundleId(TenantId tenantId, WidgetsBundleId bundleId);
PageData<WidgetTypeId> findAllWidgetTypesIds(PageLink pageLink);
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java

@ -25,6 +25,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -41,6 +42,10 @@ public class Dashboard extends DashboardInfo implements ExportableEntity<Dashboa
@Setter @Setter
private DashboardId externalId; private DashboardId externalId;
@Getter
@Setter
private List<ResourceExportData> resources;
public Dashboard() { public Dashboard() {
super(); super();
} }
@ -57,6 +62,7 @@ public class Dashboard extends DashboardInfo implements ExportableEntity<Dashboa
super(dashboard); super(dashboard);
this.configuration = dashboard.getConfiguration(); this.configuration = dashboard.getConfiguration();
this.externalId = dashboard.getExternalId(); this.externalId = dashboard.getExternalId();
this.resources = dashboard.getResources() != null ? new ArrayList<>(dashboard.getResources()) : null;
} }
@Schema(description = "JSON object with main configuration of the dashboard: layouts, widgets, aliases, etc. " + @Schema(description = "JSON object with main configuration of the dashboard: layouts, widgets, aliases, etc. " +

2
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -52,7 +52,9 @@ public class DataConstants {
public static final String HTTP_TRANSPORT_NAME = "HTTP"; public static final String HTTP_TRANSPORT_NAME = "HTTP";
public static final String SNMP_TRANSPORT_NAME = "SNMP"; public static final String SNMP_TRANSPORT_NAME = "SNMP";
public static final String MAXIMUM_NUMBER_OF_DEVICES_REACHED = "Maximum number of devices reached!"; public static final String MAXIMUM_NUMBER_OF_DEVICES_REACHED = "Maximum number of devices reached!";
public static final String TB_IMAGE_PREFIX = "tb-image;"; public static final String TB_IMAGE_PREFIX = "tb-image;";
public static final String TB_RESOURCE_PREFIX = "tb-resource;";
public static String[] allScopes() { public static String[] allScopes() {

2
common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java

@ -22,7 +22,6 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TbResourceId;
@Schema @Schema
@Slf4j @Slf4j
@ -33,7 +32,6 @@ import org.thingsboard.server.common.data.id.TbResourceId;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class ResourceExportData { public class ResourceExportData {
private TbResourceId id;
private String title; private String title;
private ResourceType type; private ResourceType type;
private ResourceSubType subType; private ResourceSubType subType;

7
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java

@ -114,11 +114,12 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getLink() { public String getLink() {
String scope = (tenantId != null && tenantId.isSysTenantId()) ? "system" : "tenant"; // tenantId is null in case of export to git
if (resourceType == ResourceType.IMAGE) { if (resourceType == ResourceType.IMAGE) {
String type = (tenantId != null && tenantId.isSysTenantId()) ? "system" : "tenant"; // tenantId is null in case of export to git return "/api/images/" + scope + "/" + resourceKey;
return "/api/images/" + type + "/" + resourceKey; } else {
return "/api/resource/" + resourceType.name().toLowerCase() + "/" + scope + "/" + resourceKey;
} }
return null;
} }
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)

28
common/data/src/main/java/org/thingsboard/server/common/data/dashboard/DashboardExportData.java

@ -1,28 +0,0 @@
/**
* 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.common.data.dashboard;
import lombok.Data;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData;
import java.util.List;
@Data
public class DashboardExportData {
private Dashboard dashboard;
private List<ResourceExportData> resources;
}

27
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetExportData.java

@ -1,27 +0,0 @@
/**
* 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.common.data.widget;
import lombok.Data;
import org.thingsboard.server.common.data.ResourceExportData;
import java.util.List;
@Data
public class WidgetExportData {
private WidgetTypeDetails widgetTypeDetails;
private List<ResourceExportData> resources;
}

7
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -23,10 +23,14 @@ import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.ArrayList;
import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId"}) @JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId"})
@ -44,6 +48,8 @@ public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantI
private WidgetTypeId externalId; private WidgetTypeId externalId;
private List<ResourceExportData> resources;
public WidgetTypeDetails() { public WidgetTypeDetails() {
super(); super();
} }
@ -62,6 +68,7 @@ public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantI
this.description = widgetTypeDetails.getDescription(); this.description = widgetTypeDetails.getDescription();
this.tags = widgetTypeDetails.getTags(); this.tags = widgetTypeDetails.getTags();
this.externalId = widgetTypeDetails.getExternalId(); this.externalId = widgetTypeDetails.getExternalId();
this.resources = widgetTypeDetails.getResources() != null ? new ArrayList<>(widgetTypeDetails.getResources()) : null;
} }
} }

28
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsExportData.java

@ -1,28 +0,0 @@
/**
* 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.common.data.widget;
import lombok.Data;
import org.thingsboard.server.common.data.ResourceExportData;
import java.util.List;
@Data
public class WidgetsExportData {
private List<WidgetTypeDetails> widgetTypesDetails;
private List<ResourceExportData> resources;
}

11
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -50,6 +50,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ImageService; 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.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
@ -83,6 +84,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
@Autowired @Autowired
private ImageService imageService; private ImageService imageService;
@Autowired
private ResourceService resourceService;
@Autowired @Autowired
private DataValidator<Dashboard> dashboardValidator; private DataValidator<Dashboard> dashboardValidator;
@ -158,6 +162,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
} }
try { try {
imageService.replaceBase64WithImageUrl(dashboard); imageService.replaceBase64WithImageUrl(dashboard);
resourceService.replaceResourcesUsageWithUrls(dashboard);
var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); var saved = dashboardDao.save(dashboard.getTenantId(), dashboard);
publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId()));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId())
@ -390,6 +396,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return dashboardDao.existsById(tenantId, dashboardId.getId()); return dashboardDao.existsById(tenantId, dashboardId.getId());
} }
@Override
public PageData<DashboardId> findAllDashboardsIds(PageLink pageLink) {
return dashboardDao.findAllIds(pageLink);
}
private final PaginatedRemover<TenantId, DashboardId> tenantDashboardsRemover = new PaginatedRemover<>() { private final PaginatedRemover<TenantId, DashboardId> tenantDashboardsRemover = new PaginatedRemover<>() {
@Override @Override

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

@ -56,7 +56,6 @@ import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage;
import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetTypeDao;
import org.thingsboard.server.dao.widget.WidgetsBundleDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collections; import java.util.Collections;
@ -568,24 +567,6 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return null; return null;
} }
private String encode(String data) {
return encode(data.getBytes(StandardCharsets.UTF_8));
}
private String encode(byte[] data) {
if (data == null || data.length == 0) {
return "";
}
return Base64.getEncoder().encodeToString(data);
}
private String decode(String value) {
if (value == null) {
return null;
}
return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
}
@Data(staticConstructor = "of") @Data(staticConstructor = "of")
private static class UpdateResult { private static class UpdateResult {
private final boolean updated; private final boolean updated;

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

@ -30,6 +30,7 @@ import org.thingsboard.common.util.RegexUtils;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
@ -52,12 +53,15 @@ import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;
import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
@ -282,64 +286,104 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
} }
@Override @Override
public List<TbResourceInfo> processResourcesForExport(Dashboard dashboard) { public boolean replaceResourcesUsageWithUrls(Dashboard dashboard) {
return processResourcesForExport(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING); return replaceResourcesUsageWithUrls(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING);
} }
@Override @Override
public List<TbResourceInfo> processResourcesForExport(WidgetTypeDetails widgetTypeDetails) { public boolean replaceResourcesUsageWithUrls(WidgetTypeDetails widgetTypeDetails) {
return processResourcesForExport(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); return replaceResourcesUsageWithUrls(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING);
} }
private List<TbResourceInfo> processResourcesForExport(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) { private boolean replaceResourcesUsageWithUrls(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
List<TbResourceInfo> resources = new ArrayList<>(); AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> { processResources(jsonNode, mapping, value -> {
if (StringUtils.isBlank(value)) { if (value.startsWith(DataConstants.TB_RESOURCE_PREFIX + "/api/resource")) { // already a link, ignoring
return value; return value;
} }
TbResourceId resourceId; TbResourceInfo resourceInfo;
try { if (value.startsWith("tb-resource:")) { // tag with metadata, probably importing
resourceId = new TbResourceId(UUID.fromString(value)); String[] metadata = StringUtils.removeStart(value, "tb-resource:").split(":");
} catch (IllegalArgumentException e) { if (metadata.length < 2) {
return value; return value;
}
ResourceType resourceType = ResourceType.valueOf(decode(metadata[0]));
String etag = metadata[1];
resourceInfo = findSystemOrTenantResourceByEtag(tenantId, resourceType, etag);
if (resourceInfo == null) {
return value;
}
} else { // probably importing an old dashboard json where resources are referenced by ids
TbResourceId resourceId;
try {
resourceId = new TbResourceId(UUID.fromString(value));
} catch (IllegalArgumentException e) {
return value;
}
resourceInfo = findResourceInfoById(tenantId, resourceId);
if (resourceInfo == null) {
updated.set(true);
return "";
}
} }
TbResourceInfo resourceInfo = findResourceInfoById(tenantId, resourceId);
resources.add(resourceInfo); updated.set(true);
return value; return DataConstants.TB_RESOURCE_PREFIX + resourceInfo.getLink();
}); });
return resources; return updated.get();
} }
@Override @Override
public void processResourcesForImport(Dashboard dashboard, Map<TbResourceId, TbResourceId> importedResources) { public List<TbResourceInfo> replaceResourcesUrlsWithTags(Dashboard dashboard) {
processResourcesForImport(dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING, importedResources); return replaceResourcesUrlsWithTags(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING);
} }
@Override @Override
public void processResourcesForImport(WidgetTypeDetails widgetTypeDetails, Map<TbResourceId, TbResourceId> importedResources) { public List<TbResourceInfo> replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails) {
processResourcesForImport(widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING, importedResources); return replaceResourcesUrlsWithTags(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING);
} }
private void processResourcesForImport(JsonNode jsonNode, Map<String, String> mapping, Map<TbResourceId, TbResourceId> importedResources) { private List<TbResourceInfo> replaceResourcesUrlsWithTags(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> { List<TbResourceInfo> resources = new ArrayList<>();
if (StringUtils.isBlank(value)) { processResources(jsonNode, mapping, value -> {
if (!value.startsWith(DataConstants.TB_RESOURCE_PREFIX + "/api/resource/")) {
return value; return value;
} }
TbResourceId oldResourceId; ResourceType resourceType;
String resourceKey;
TenantId resourceTenantId;
try { try {
oldResourceId = new TbResourceId(UUID.fromString(value)); String[] parts = StringUtils.removeStart(value, DataConstants.TB_RESOURCE_PREFIX + "/api/resource/").split("/");
} catch (IllegalArgumentException e) { resourceType = ResourceType.valueOf(parts[0].toUpperCase());
String scope = parts[1];
resourceKey = parts[2];
resourceTenantId = scope.equals("system") ? TenantId.SYS_TENANT_ID : tenantId;
} catch (Exception e) {
log.warn("[{}] Invalid resource link '{}'", tenantResourcesRemover, value);
return value; return value;
} }
TbResourceId importedResourceId = importedResources.get(oldResourceId);
if (importedResourceId != null) { TbResourceInfo resourceInfo = findResourceInfoByTenantIdAndKey(resourceTenantId, resourceType, resourceKey);
return importedResourceId.toString(); if (resourceInfo != null) {
resources.add(resourceInfo);
return "tb-resource:" + String.join(":", encode(resourceType.name()), resourceInfo.getEtag());
} else { } else {
return value; return value;
} }
}); });
return resources;
}
private void processResources(JsonNode jsonNode, Map<String, String> mapping, UnaryOperator<String> processor) {
JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> {
if (StringUtils.isBlank(value)) {
return value;
}
return processor.apply(value);
});
} }
@Override @Override
@ -391,6 +435,24 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceInfoDao.findSystemOrTenantResourceByEtag(tenantId, resourceType, etag); return resourceInfoDao.findSystemOrTenantResourceByEtag(tenantId, resourceType, etag);
} }
protected String encode(String data) {
return encode(data.getBytes(StandardCharsets.UTF_8));
}
protected String encode(byte[] data) {
if (data == null || data.length == 0) {
return "";
}
return Base64.getEncoder().encodeToString(data);
}
protected String decode(String value) {
if (value == null) {
return null;
}
return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
}
private final PaginatedRemover<TenantId, TbResourceId> tenantResourcesRemover = new PaginatedRemover<>() { private final PaginatedRemover<TenantId, TbResourceId> tenantResourcesRemover = new PaginatedRemover<>() {
@Override @Override

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

@ -37,6 +37,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.resource.ImageService; 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.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
@ -68,6 +69,9 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
@Autowired @Autowired
protected ImageService imageService; protected ImageService imageService;
@Autowired
private ResourceService resourceService;
@Override @Override
public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) { public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) {
log.trace("Executing findWidgetTypeById [{}]", widgetTypeId); log.trace("Executing findWidgetTypeById [{}]", widgetTypeId);
@ -95,6 +99,8 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId); widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId);
try { try {
imageService.replaceBase64WithImageUrl(widgetTypeDetails); imageService.replaceBase64WithImageUrl(widgetTypeDetails);
resourceService.replaceResourcesUsageWithUrls(widgetTypeDetails);
WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId())
.entityId(result.getId()).created(widgetTypeDetails.getId() == null).build()); .entityId(result.getId()).created(widgetTypeDetails.getId() == null).build());
@ -248,6 +254,11 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
bundleWidgetTypesRemover.removeEntities(tenantId, bundleId); bundleWidgetTypesRemover.removeEntities(tenantId, bundleId);
} }
@Override
public PageData<WidgetTypeId> findAllWidgetTypesIds(PageLink pageLink) {
return widgetTypeDao.findAllWidgetTypesIds(pageLink);
}
@Override @Override
public void deleteByTenantId(TenantId tenantId) { public void deleteByTenantId(TenantId tenantId) {
deleteWidgetTypesByTenantId(tenantId); deleteWidgetTypesByTenantId(tenantId);

6
ui-ngx/src/app/core/http/dashboard.service.ts

@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { Dashboard, DashboardExportData, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models'; import { Dashboard, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { filter, map, publishReplay, refCount } from 'rxjs/operators'; import { filter, map, publishReplay, refCount } from 'rxjs/operators';
@ -71,8 +71,8 @@ export class DashboardService {
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptionsFromConfig(config));
} }
public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<DashboardExportData> { public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<Dashboard> {
return this.http.get<DashboardExportData>(`/api/dashboard/${dashboardId}/export`, defaultHttpOptionsFromConfig(config)); return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}?inlineImages=true`, defaultHttpOptionsFromConfig(config));
} }
public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> { public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> {

7
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -144,12 +144,10 @@ export class ImportExportService {
public exportDashboard(dashboardId: string) { public exportDashboard(dashboardId: string) {
this.dashboardService.exportDashboard(dashboardId).subscribe({ this.dashboardService.exportDashboard(dashboardId).subscribe({
next: (exportData) => { next: (dashboard) => {
let dashboard = exportData.dashboard;
let name = dashboard.title; let name = dashboard.title;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
exportData.dashboard = this.prepareDashboardExport(dashboard) this.exportToPc(this.prepareDashboardExport(dashboard), name);
this.exportToPc(exportData, name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'dashboard.export-failed-error'); this.handleExportError(e, 'dashboard.export-failed-error');
@ -157,7 +155,6 @@ export class ImportExportService {
}); });
} }
// FIXME: backward compatibility - support old export structure
public importDashboard(onEditMissingAliases: editMissingAliasesFunction): Observable<Dashboard> { public importDashboard(onEditMissingAliases: editMissingAliasesFunction): Observable<Dashboard> {
return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe( return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe(
mergeMap((dashboard: Dashboard) => { mergeMap((dashboard: Dashboard) => {

6
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -194,6 +194,7 @@ export interface DashboardConfiguration {
export interface Dashboard extends DashboardInfo { export interface Dashboard extends DashboardInfo {
configuration?: DashboardConfiguration; configuration?: DashboardConfiguration;
dialogRef?: MatDialogRef<any>; dialogRef?: MatDialogRef<any>;
resources?: Array<any>;
} }
export interface HomeDashboard extends Dashboard { export interface HomeDashboard extends Dashboard {
@ -209,11 +210,6 @@ export interface DashboardSetup extends Dashboard {
assignedCustomerIds?: Array<string>; assignedCustomerIds?: Array<string>;
} }
export interface DashboardExportData {
dashboard?: Dashboard;
resources: any;
}
export const isPublicDashboard = (dashboard: DashboardInfo): boolean => { export const isPublicDashboard = (dashboard: DashboardInfo): boolean => {
if (dashboard && dashboard.assignedCustomers) { if (dashboard && dashboard.assignedCustomers) {
return dashboard.assignedCustomers return dashboard.assignedCustomers

1
ui-ngx/src/app/shared/models/widget.models.ts

@ -249,6 +249,7 @@ export interface WidgetTypeDetails extends WidgetType, ExportableEntity<WidgetTy
image: string; image: string;
description: string; description: string;
tags: string[]; tags: string[];
resources?: Array<any>;
} }
export enum DeprecatedFilter { export enum DeprecatedFilter {

Loading…
Cancel
Save