Change dashboard export structure; images export-import improvements

This commit is contained in:
ViacheslavKlimov 2024-10-15 17:08:15 +03:00
parent 2a71abbd52
commit cb097bb637
24 changed files with 554 additions and 242 deletions

View File

@ -26,7 +26,10 @@ import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; 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.DeleteMapping;
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;
@ -42,6 +45,7 @@ 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;
@ -111,8 +115,7 @@ public class DashboardController extends BaseController {
notes = "Get the server time (milliseconds since January 1, 1970 UTC). " + notes = "Get the server time (milliseconds since January 1, 1970 UTC). " +
"Used to adjust view of the dashboards according to the difference between browser and server time.") "Used to adjust view of the dashboards according to the difference between browser and server time.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET) @GetMapping(value = "/dashboard/serverTime")
@ResponseBody
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137"))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137")))
public long getServerTime() throws ThingsboardException { public long getServerTime() throws ThingsboardException {
return System.currentTimeMillis(); return System.currentTimeMillis();
@ -124,8 +127,7 @@ public class DashboardController extends BaseController {
"It also impacts the 'Grouping interval' in case of any other 'Data aggregation function' is selected. " + "It also impacts the 'Grouping interval' in case of any other 'Data aggregation function' is selected. " +
"The actual value of the limit is configurable in the system configuration file.") "The actual value of the limit is configurable in the system configuration file.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/maxDatapointsLimit", method = RequestMethod.GET) @GetMapping(value = "/dashboard/maxDatapointsLimit")
@ResponseBody
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000"))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000")))
public long getMaxDatapointsLimit() throws ThingsboardException { public long getMaxDatapointsLimit() throws ThingsboardException {
return maxDatapointsLimit; return maxDatapointsLimit;
@ -134,8 +136,7 @@ public class DashboardController extends BaseController {
@ApiOperation(value = "Get Dashboard Info (getDashboardInfoById)", @ApiOperation(value = "Get Dashboard Info (getDashboardInfoById)",
notes = "Get the information about the dashboard based on 'dashboardId' parameter. " + DASHBOARD_INFO_DEFINITION) notes = "Get the information about the dashboard based on 'dashboardId' parameter. " + DASHBOARD_INFO_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET) @GetMapping(value = "/dashboard/info/{dashboardId}")
@ResponseBody
public DashboardInfo getDashboardInfoById( public DashboardInfo getDashboardInfoById(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
@ -148,8 +149,7 @@ public class DashboardController extends BaseController {
notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
) )
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) @GetMapping(value = "/dashboard/{dashboardId}")
@ResponseBody
public Dashboard getDashboardById( public Dashboard getDashboardById(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId, @PathVariable(DASHBOARD_ID) String strDashboardId,
@ -164,6 +164,15 @@ public class DashboardController extends BaseController {
return result; return result;
} }
@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);
return tbDashboardService.exportDashboard(getTenantId(), dashboard, getCurrentUser());
}
@ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)",
notes = "Create or update the Dashboard. When creating dashboard, platform generates Dashboard Id as " + UUID_WIKI_LINK + notes = "Create or update the Dashboard. When creating dashboard, platform generates Dashboard Id as " + UUID_WIKI_LINK +
"The newly created Dashboard id will be present in the response. " + "The newly created Dashboard id will be present in the response. " +
@ -172,24 +181,30 @@ public class DashboardController extends BaseController {
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/dashboard", method = RequestMethod.POST) @PostMapping(value = "/dashboard")
@ResponseBody public Dashboard saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
public Dashboard saveDashboard( @RequestBody Dashboard dashboard) throws Exception {
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
@RequestBody Dashboard dashboard) throws Exception {
dashboard.setTenantId(getTenantId()); dashboard.setTenantId(getTenantId());
checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD); checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
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')")
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/dashboard/{dashboardId}")
@ResponseStatus(value = HttpStatus.OK) public void deleteDashboard(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
public void deleteDashboard( @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
checkParameter(DASHBOARD_ID, strDashboardId); checkParameter(DASHBOARD_ID, strDashboardId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE);
@ -200,8 +215,7 @@ public class DashboardController extends BaseController {
notes = "Assign the Dashboard to specified Customer or do nothing if the Dashboard is already assigned to that Customer. " + notes = "Assign the Dashboard to specified Customer or do nothing if the Dashboard is already assigned to that Customer. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.POST) @PostMapping(value = "/customer/{customerId}/dashboard/{dashboardId}")
@ResponseBody
public Dashboard assignDashboardToCustomer( public Dashboard assignDashboardToCustomer(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -222,8 +236,7 @@ public class DashboardController extends BaseController {
notes = "Unassign the Dashboard from specified Customer or do nothing if the Dashboard is already assigned to that Customer. " + notes = "Unassign the Dashboard from specified Customer or do nothing if the Dashboard is already assigned to that Customer. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/customer/{customerId}/dashboard/{dashboardId}")
@ResponseBody
public Dashboard unassignDashboardFromCustomer( public Dashboard unassignDashboardFromCustomer(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -243,8 +256,7 @@ public class DashboardController extends BaseController {
"Returns the Dashboard object. " + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/dashboard/{dashboardId}/customers", method = RequestMethod.POST) @PostMapping(value = "/dashboard/{dashboardId}/customers")
@ResponseBody
public Dashboard updateDashboardCustomers( public Dashboard updateDashboardCustomers(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId, @PathVariable(DASHBOARD_ID) String strDashboardId,
@ -261,8 +273,7 @@ public class DashboardController extends BaseController {
notes = "Adds the list of Customers to the existing list of assignments for the Dashboard. Keeps previous assignments to customers that are not in the provided list. " + notes = "Adds the list of Customers to the existing list of assignments for the Dashboard. Keeps previous assignments to customers that are not in the provided list. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/dashboard/{dashboardId}/customers/add", method = RequestMethod.POST) @PostMapping(value = "/dashboard/{dashboardId}/customers/add")
@ResponseBody
public Dashboard addDashboardCustomers( public Dashboard addDashboardCustomers(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId, @PathVariable(DASHBOARD_ID) String strDashboardId,
@ -279,8 +290,7 @@ public class DashboardController extends BaseController {
notes = "Removes the list of Customers from the existing list of assignments for the Dashboard. Keeps other assignments to customers that are not in the provided list. " + notes = "Removes the list of Customers from the existing list of assignments for the Dashboard. Keeps other assignments to customers that are not in the provided list. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/dashboard/{dashboardId}/customers/remove", method = RequestMethod.POST) @PostMapping(value = "/dashboard/{dashboardId}/customers/remove")
@ResponseBody
public Dashboard removeDashboardCustomers( public Dashboard removeDashboardCustomers(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId, @PathVariable(DASHBOARD_ID) String strDashboardId,
@ -655,4 +665,5 @@ public class DashboardController extends BaseController {
} }
return customerIds; return customerIds;
} }
} }

View File

@ -40,7 +40,7 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbImageDeleteResult;
@ -61,7 +61,6 @@ 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.Base64;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@ -193,53 +192,19 @@ public class ImageController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = IMAGE_URL + "/export") @GetMapping(value = IMAGE_URL + "/export")
public ImageExportData exportImage(@Parameter(description = IMAGE_TYPE_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"tenant", "system"}), required = true) public ResourceExportData exportImage(@Parameter(description = IMAGE_TYPE_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"tenant", "system"}), required = true)
@PathVariable String type, @PathVariable String type,
@Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true)
@PathVariable String key) throws Exception { @PathVariable String key) throws Exception {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); return tbImageService.exportImage(imageInfo);
byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId());
return ImageExportData.builder()
.mediaType(descriptor.getMediaType())
.fileName(imageInfo.getFileName())
.title(imageInfo.getTitle())
.subType(imageInfo.getResourceSubType().name())
.resourceKey(imageInfo.getResourceKey())
.isPublic(imageInfo.isPublic())
.publicResourceKey(imageInfo.getPublicResourceKey())
.data(Base64.getEncoder().encodeToString(data))
.build();
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping("/api/image/import") @PutMapping("/api/image/import")
public TbResourceInfo importImage(@RequestBody ImageExportData imageData) throws Exception { public TbResourceInfo importImage(@RequestBody ResourceExportData imageData) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
TbResource image = new TbResource(); return tbImageService.importImage(imageData, false, user);
image.setTenantId(user.getTenantId());
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
image.setFileName(imageData.getFileName());
if (StringUtils.isNotEmpty(imageData.getTitle())) {
image.setTitle(imageData.getTitle());
} else {
image.setTitle(imageData.getFileName());
}
if (StringUtils.isNotEmpty(imageData.getSubType())) {
image.setResourceSubType(ResourceSubType.valueOf(imageData.getSubType()));
} else {
image.setResourceSubType(ResourceSubType.IMAGE);
}
image.setResourceType(ResourceType.IMAGE);
image.setResourceKey(imageData.getResourceKey());
image.setPublic(imageData.isPublic());
image.setPublicResourceKey(imageData.getPublicResourceKey());
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(imageData.getMediaType());
image.setDescriptorValue(descriptor);
image.setData(Base64.getDecoder().decode(imageData.getData()));
return tbImageService.save(image, user);
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")

View File

@ -21,7 +21,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; 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.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;
@ -39,12 +41,12 @@ 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.WidgetsBundleFilter;
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.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ImageService;
@ -56,6 +58,7 @@ 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.INLINE_IMAGES; import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
@ -93,8 +96,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type Details (getWidgetTypeById)", @ApiOperation(value = "Get Widget Type Details (getWidgetTypeById)",
notes = "Get the Widget Type Details based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Get the Widget Type Details based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.GET) @GetMapping(value = "/widgetType/{widgetTypeId}")
@ResponseBody
public WidgetTypeDetails getWidgetTypeById( public WidgetTypeDetails getWidgetTypeById(
@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId, @PathVariable("widgetTypeId") String strWidgetTypeId,
@ -109,6 +111,14 @@ public class WidgetTypeController extends AutoCommitController {
return result; return result;
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetType/{widgetTypeId}/export")
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)",
notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -151,6 +161,15 @@ 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')")

View File

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
@ -71,6 +72,8 @@ public abstract class AbstractTbEntityService {
@Autowired(required = false) @Autowired(required = false)
@Lazy @Lazy
private EntitiesVersionControlService vcService; private EntitiesVersionControlService vcService;
@Autowired
protected AccessControlService accessControlService;
protected boolean isTestProfile() { protected boolean isTestProfile() {
return Set.of(this.env.getActiveProfiles()).contains("test"); return Set.of(this.env.getActiveProfiles()).contains("test");

View File

@ -20,9 +20,13 @@ 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;
@ -30,10 +34,16 @@ 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.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.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.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
@Service @Service
@ -42,6 +52,8 @@ 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;
@Override @Override
public Dashboard save(Dashboard dashboard, User user) throws Exception { public Dashboard save(Dashboard dashboard, User user) throws Exception {
@ -283,4 +295,29 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
} }
} }
@Override
public DashboardExportData exportDashboard(TenantId tenantId, Dashboard dashboard, SecurityUser user) throws ThingsboardException {
List<TbResourceInfo> images = imageService.inlineImages(dashboard);
for (TbResourceInfo imageInfo : images) {
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, imageInfo.getId(), imageInfo);
}
DashboardExportData exportData = new DashboardExportData();
exportData.setDashboard(dashboard);
exportData.setResources(images.stream()
.map(tbImageService::exportImage)
.toList());
return exportData;
}
@Override
public Dashboard importDashboard(DashboardExportData exportData, SecurityUser user) throws Exception {
for (ResourceExportData resourceExportData : exportData.getResources()) {
if (resourceExportData.getType() == ResourceType.IMAGE) {
tbImageService.importImage(resourceExportData, true, user);
}
}
return save(exportData.getDashboard(), user);
}
} }

View File

@ -18,12 +18,14 @@ 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;
@ -47,4 +49,8 @@ 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;
} }

View File

@ -18,15 +18,27 @@ package org.thingsboard.server.service.entitiy.widgets.type;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
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.TenantId; import org.thingsboard.server.common.data.id.TenantId;
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.dao.resource.ImageService;
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.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
@Service @Service
@TbCoreComponent @TbCoreComponent
@ -35,6 +47,8 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
private final WidgetTypeService widgetTypeService; private final WidgetTypeService widgetTypeService;
private final ImageService imageService;
private final TbImageService tbImageService;
@Override @Override
public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception { public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception {
@ -75,4 +89,30 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
throw e; throw e;
} }
} }
@Override
public WidgetExportData exportWidgetType(TenantId tenantId, WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException {
List<TbResourceInfo> images = imageService.inlineImages(widgetTypeDetails);
for (TbResourceInfo imageInfo : images) {
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, imageInfo.getId(), imageInfo);
}
WidgetExportData exportData = new WidgetExportData();
exportData.setWidgetTypeDetails(widgetTypeDetails);
exportData.setResources(images.stream()
.map(tbImageService::exportImage)
.toList());
return exportData;
}
@Override
public WidgetTypeDetails importWidgetType(WidgetExportData exportData, SecurityUser user) throws Exception {
for (ResourceExportData resourceExportData : exportData.getResources()) {
if (resourceExportData.getType() == ResourceType.IMAGE) {
tbImageService.importImage(resourceExportData, true, user);
}
}
return save(exportData.getWidgetTypeDetails(), user);
}
} }

View File

@ -16,11 +16,19 @@
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.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
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.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
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, User user) throws Exception;
WidgetExportData exportWidgetType(TenantId tenantId, WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException;
WidgetTypeDetails importWidgetType(WidgetExportData exportData, SecurityUser user) throws Exception;
} }

View File

@ -24,7 +24,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
@ -37,13 +39,19 @@ import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
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 org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
@Service @Service
@Slf4j @Slf4j
@TbCoreComponent @TbCoreComponent
@ -89,7 +97,7 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb
try { try {
var oldEtag = getEtag(image); var oldEtag = getEtag(image);
TbResourceInfo existingImage = null; TbResourceInfo existingImage = null;
if (image.getId() == null && StringUtils.isNotEmpty(image.getResourceKey())) { if (image.getId() == null && isNotEmpty(image.getResourceKey())) {
existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey()); existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey());
if (existingImage != null) { if (existingImage != null) {
image.setId(existingImage.getId()); image.setId(existingImage.getId());
@ -174,6 +182,60 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb
} }
} }
@Override
public ResourceExportData exportImage(TbResourceInfo imageInfo) {
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId());
return ResourceExportData.builder()
.mediaType(descriptor.getMediaType())
.fileName(imageInfo.getFileName())
.title(imageInfo.getTitle())
.type(ResourceType.IMAGE)
.subType(imageInfo.getResourceSubType())
.resourceKey(imageInfo.getResourceKey())
.isPublic(imageInfo.isPublic())
.publicResourceKey(imageInfo.getPublicResourceKey())
.data(Base64.getEncoder().encodeToString(data))
.build();
}
@Override
public TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception {
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
byte[] data = Base64.getDecoder().decode(imageData.getData());
if (checkExisting) {
String etag = imageService.calculateImageEtag(data);
TbResourceInfo existingImage = imageService.findSystemOrTenantImageByEtag(user.getTenantId(), etag);
if (existingImage != null) {
return existingImage;
}
}
image.setFileName(imageData.getFileName());
if (isNotEmpty(imageData.getTitle())) {
image.setTitle(imageData.getTitle());
} else {
image.setTitle(imageData.getFileName());
}
if (imageData.getSubType() != null) {
image.setResourceSubType(imageData.getSubType());
} else {
image.setResourceSubType(ResourceSubType.IMAGE);
}
image.setResourceType(ResourceType.IMAGE);
image.setResourceKey(imageData.getResourceKey());
image.setPublic(imageData.isPublic());
image.setPublicResourceKey(imageData.getPublicResourceKey());
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(imageData.getMediaType());
image.setDescriptorValue(descriptor);
image.setData(data);
return save(image, user);
}
private void evictFromCache(TenantId tenantId, List<ImageCacheKey> toEvict) { private void evictFromCache(TenantId tenantId, List<ImageCacheKey> toEvict) {
toEvict.forEach(this::evictETags); toEvict.forEach(this::evictETags);
clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder() clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder()

View File

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.service.resource; package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbImageService { public interface TbImageService {
@ -35,4 +37,8 @@ public interface TbImageService {
void evictETags(ImageCacheKey imageCacheKey); void evictETags(ImageCacheKey imageCacheKey);
ResourceExportData exportImage(TbResourceInfo imageInfo);
TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception;
} }

View File

@ -41,18 +41,22 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockPart;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
@ -74,6 +78,7 @@ import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
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.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -131,6 +136,7 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -1162,4 +1168,19 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return oAuth2Client; return oAuth2Client;
} }
protected <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception {
return this.uploadImage(httpMethod, url, null, filename, mediaType, content);
}
protected <R> TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception {
MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content);
var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file);
if (StringUtils.isNotEmpty(subType)) {
var imageSubTypePart = new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8));
request.part(imageSubTypePart);
}
setJwtToken(request);
return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class);
}
} }

View File

@ -26,18 +26,22 @@ import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
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.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.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.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;
@ -49,6 +53,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -575,7 +580,37 @@ public class DashboardControllerTest extends AbstractControllerTest {
.andReturn().getResponse().getContentAsString(); .andReturn().getResponse().getContentAsString();
String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText(); String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText();
assertThat(errorMessage).containsIgnoringCase("referenced by an asset profile"); assertThat(errorMessage).containsIgnoringCase("referenced by an asset profile");
}
@Test
public void testExportImportDashboard() throws Exception {
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", "image12.png", "image/png", ImageControllerTest.PNG_IMAGE);
Dashboard dashboard = new Dashboard();
dashboard.setTitle("My dashboard");
dashboard.setConfiguration(JacksonUtil.newObjectNode()
.put("someImage", "tb-image;/api/images/tenant/" + imageInfo.getResourceKey()));
dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
DashboardExportData dashboardExportData = doGet("/api/dashboard/" + dashboard.getUuidId() + "/export", DashboardExportData.class);
String imageRef = dashboardExportData.getDashboard().getConfiguration().get("someImage").asText();
assertThat(imageRef).isEqualTo("tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes()) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getName().getBytes()) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes()) + ":"
+ imageInfo.getEtag() + ";data:image/png;base64,");
List<ResourceExportData> resources = dashboardExportData.getResources();
assertThat(resources).singleElement().satisfies(resource -> {
assertThat(resource.getResourceKey()).isEqualTo(imageInfo.getResourceKey());
assertThat(resource.getData()).isEqualTo(Base64.getEncoder().encodeToString(ImageControllerTest.PNG_IMAGE));
});
doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk());
doDelete("/api/images/tenant/" + imageInfo.getResourceKey()).andExpect(status().isOk());
Dashboard importedDashboard = doPost("/api/dashboard/import", dashboardExportData, Dashboard.class);
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);
assertThat(importedImageInfo.getEtag()).isEqualTo(imageInfo.getEtag());
} }
private Dashboard createDashboard(String title) { private Dashboard createDashboard(String title) {

File diff suppressed because one or more lines are too long

View File

@ -27,6 +27,8 @@ 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.common.data.widget.WidgetTypeDetails;
import java.util.List;
public interface ImageService { public interface ImageService {
TbResourceInfo saveImage(TbResource image); TbResourceInfo saveImage(TbResource image);
@ -59,13 +61,14 @@ public interface ImageService {
void inlineImage(HasImage entity); void inlineImage(HasImage entity);
void inlineImages(Dashboard dashboard); List<TbResourceInfo> inlineImages(Dashboard dashboard);
void inlineImages(WidgetTypeDetails widgetTypeDetails); List<TbResourceInfo> inlineImages(WidgetTypeDetails widgetTypeDetails);
void inlineImageForEdge(HasImage entity); void inlineImageForEdge(HasImage entity);
void inlineImagesForEdge(Dashboard dashboard); void inlineImagesForEdge(Dashboard dashboard);
void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails); void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails);
} }

View File

@ -28,15 +28,16 @@ import lombok.extern.slf4j.Slf4j;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class ImageExportData { public class ResourceExportData {
private String mediaType;
private String fileName;
private String title; private String title;
private String subType; private ResourceType type;
private ResourceSubType subType;
private String resourceKey; private String resourceKey;
private boolean isPublic; private String fileName;
private String publicResourceKey; private String publicResourceKey;
private boolean isPublic;
private String mediaType;
private String data; private String data;
} }

View File

@ -17,11 +17,11 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -134,11 +134,12 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
return title; return title;
} }
public <T> T getDescriptor(Class<T> type) throws JsonProcessingException { @SneakyThrows
public <T> T getDescriptor(Class<T> type) {
return descriptor != null ? mapper.treeToValue(descriptor, type) : null; return descriptor != null ? mapper.treeToValue(descriptor, type) : null;
} }
public <T> void updateDescriptor(Class<T> type, UnaryOperator<T> updater) throws JsonProcessingException { public <T> void updateDescriptor(Class<T> type, UnaryOperator<T> updater) {
T descriptor = getDescriptor(type); T descriptor = getDescriptor(type);
descriptor = updater.apply(descriptor); descriptor = updater.apply(descriptor);
setDescriptorValue(descriptor); setDescriptorValue(descriptor);

View File

@ -0,0 +1,28 @@
/**
* 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;
}

View File

@ -0,0 +1,27 @@
/**
* 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;
}

View File

@ -32,7 +32,9 @@ import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
@ -46,10 +48,13 @@ import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -265,7 +270,7 @@ public class JacksonUtil {
return value != null ? OBJECT_MAPPER.readTree(value) : null; return value != null ? OBJECT_MAPPER.readTree(value) : null;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException("The given InputStream value: " throw new IllegalArgumentException("The given InputStream value: "
+ value + " cannot be transformed to a JsonNode", e); + value + " cannot be transformed to a JsonNode", e);
} }
} }
@ -427,4 +432,50 @@ public class JacksonUtil {
} }
} }
public static void replaceAll(JsonNode root, String pathPrefix, BiFunction<String, String, String> processor) {
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>();
tasks.add(new JsonNodeProcessingTask(pathPrefix, root));
while (!tasks.isEmpty()) {
JsonNodeProcessingTask task = tasks.poll();
JsonNode node = task.getNode();
if (node == null) {
continue;
}
String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + ".");
if (node.isObject()) {
ObjectNode on = (ObjectNode) node;
for (Iterator<String> it = on.fieldNames(); it.hasNext(); ) {
String childName = it.next();
JsonNode childValue = on.get(childName);
if (childValue.isTextual()) {
on.put(childName, processor.apply(currentPath + childName, childValue.asText()));
} else if (childValue.isObject() || childValue.isArray()) {
tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue));
}
}
} else if (node.isArray()) {
ArrayNode childArray = (ArrayNode) node;
for (int i = 0; i < childArray.size(); i++) {
JsonNode element = childArray.get(i);
if (element.isObject()) {
tasks.add(new JsonNodeProcessingTask(currentPath + "." + i, element));
} else if (element.isTextual()) {
childArray.set(i, processor.apply(currentPath + "." + i, element.asText()));
}
}
}
}
}
@Data
public static class JsonNodeProcessingTask {
private final String path;
private final JsonNode node;
public JsonNodeProcessingTask(String path, JsonNode node) {
this.path = path;
this.node = node;
}
}
} }

View File

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.dao.resource; package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@ -56,12 +55,12 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import org.thingsboard.server.dao.util.ImageUtils; import org.thingsboard.server.dao.util.ImageUtils;
import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage; import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage;
import org.thingsboard.server.dao.util.JsonNodeProcessingTask;
import org.thingsboard.server.dao.util.JsonPathProcessingTask; import org.thingsboard.server.dao.util.JsonPathProcessingTask;
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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -72,8 +71,12 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static org.apache.commons.lang3.ArrayUtils.get;
@Service @Service
@Slf4j @Slf4j
public class BaseImageService extends BaseResourceService implements ImageService { public class BaseImageService extends BaseResourceService implements ImageService {
@ -293,6 +296,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
@Override @Override
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) { public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) {
if (StringUtils.isEmpty(etag)) {
return null;
}
log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag); log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag);
return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag); return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag);
} }
@ -430,60 +436,67 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return base64ToImageUrl(tenantId, name, data, false); return base64ToImageUrl(tenantId, name, data, false);
} }
private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:([^:]*):([^:]*):?([^:]*)?;data:(.*);.*"); private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:([^;]+);data:(.*);.*");
private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) { private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) {
if (StringUtils.isBlank(data)) { if (StringUtils.isBlank(data)) {
return UpdateResult.of(false, data); return UpdateResult.of(false, data);
} }
String resourceKey = null;
String resourceName = null;
String resourceSubType = null;
String etag = null;
String mediaType;
var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data); var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data);
boolean matches = matcher.matches(); if (matcher.matches()) {
String mdResourceKey = null; String[] metadata = matcher.group(1).split(":");
String mdResourceName = null; resourceKey = decode(get(metadata, 0));
String mdResourceSubType = null; resourceName = decode(get(metadata, 1));
String mdMediaType; resourceSubType = decode(get(metadata, 2));
if (matches) { etag = get(metadata, 3);
mdResourceKey = new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8); mediaType = matcher.group(2);
mdResourceName = new String(Base64.getDecoder().decode(matcher.group(2)), StandardCharsets.UTF_8);
if (StringUtils.isNotBlank(matcher.group(3))) {
mdResourceSubType = new String(Base64.getDecoder().decode(matcher.group(3)), StandardCharsets.UTF_8);
}
mdMediaType = matcher.group(4);
} else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) { } else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) {
mdMediaType = StringUtils.substringBetween(data, "data:", ";base64"); mediaType = StringUtils.substringBetween(data, "data:", ";base64");
} else { } else {
return UpdateResult.of(false, data); return UpdateResult.of(false, data);
} }
String base64Data = StringUtils.substringAfter(data, "base64,"); String base64Data = StringUtils.substringAfter(data, "base64,");
String extension = ImageUtils.mediaTypeToFileExtension(mdMediaType); byte[] imageData = StringUtils.isNotEmpty(base64Data) ? Base64.getDecoder().decode(base64Data) : null;
byte[] imageData = Base64.getDecoder().decode(base64Data); if (StringUtils.isBlank(etag)) {
String etag = calculateEtag(imageData); etag = calculateEtag(imageData);
}
var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag); var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag);
if (imageInfo == null) { if (imageInfo == null) {
if (imageData == null) {
return UpdateResult.of(false, data);
}
TbResource image = new TbResource(); TbResource image = new TbResource();
image.setTenantId(tenantId); image.setTenantId(tenantId);
image.setResourceType(ResourceType.IMAGE); image.setResourceType(ResourceType.IMAGE);
if (StringUtils.isBlank(mdResourceName)) { if (StringUtils.isBlank(resourceName)) {
mdResourceName = name; resourceName = name;
} }
image.setTitle(mdResourceName); image.setTitle(resourceName);
String fileName; String fileName;
if (StringUtils.isBlank(mdResourceKey)) { if (StringUtils.isBlank(resourceKey)) {
fileName = StringUtils.strip(mdResourceName.toLowerCase() String extension = ImageUtils.mediaTypeToFileExtension(mediaType);
fileName = StringUtils.strip(resourceName.toLowerCase()
.replaceAll("['\"]", "") .replaceAll("['\"]", "")
.replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers .replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers
+ "." + extension; + "." + extension;
} else { } else {
fileName = mdResourceKey; fileName = resourceKey;
} }
if (StringUtils.isBlank(mdResourceSubType)) { if (StringUtils.isBlank(resourceSubType)) {
image.setResourceSubType(ResourceSubType.IMAGE); image.setResourceSubType(ResourceSubType.IMAGE);
} else { } else {
image.setResourceSubType(ResourceSubType.valueOf(mdResourceSubType)); image.setResourceSubType(ResourceSubType.valueOf(resourceSubType));
} }
image.setFileName(fileName); image.setFileName(fileName);
image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType)); image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType));
image.setData(imageData); image.setData(imageData);
image.setPublic(true); image.setPublic(true);
try { try {
@ -503,159 +516,127 @@ public class BaseImageService extends BaseResourceService implements ImageServic
} }
private boolean base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) { private boolean base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) {
boolean updated = false; AtomicBoolean updated = new AtomicBoolean(false);
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>(); JacksonUtil.replaceAll(root, title, (path, value) -> {
tasks.add(new JsonNodeProcessingTask(title, root)); UpdateResult result = base64ToImageUrl(tenantId, path, value, true);
while (!tasks.isEmpty()) { if (result.isUpdated()) {
JsonNodeProcessingTask task = tasks.poll(); updated.set(true);
JsonNode node = task.getNode();
if (node == null) {
continue;
} }
String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + " "); return result.getValue();
if (node.isObject()) { });
ObjectNode on = (ObjectNode) node; return updated.get();
for (Iterator<String> it = on.fieldNames(); it.hasNext(); ) {
String childName = it.next();
JsonNode childValue = on.get(childName);
if (childValue.isTextual()) {
UpdateResult result = base64ToImageUrl(tenantId, currentPath + childName, childValue.asText(), true);
on.put(childName, result.getValue());
updated |= result.isUpdated();
} else if (childValue.isObject() || childValue.isArray()) {
tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue));
}
}
} else if (node.isArray()) {
ArrayNode childArray = (ArrayNode) node;
for (int i = 0; i < childArray.size(); i++) {
JsonNode element = childArray.get(i);
if (element.isObject()) {
tasks.add(new JsonNodeProcessingTask(currentPath + " " + i, element));
} else if (element.isTextual()) {
UpdateResult result = base64ToImageUrl(tenantId, currentPath + "." + i, element.asText(), true);
childArray.set(i, result.getValue());
updated |= result.isUpdated();
}
}
}
}
return updated;
} }
@Override @Override
public void inlineImage(HasImage entity) { public void inlineImage(HasImage entity) {
log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName()); log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), true)); inlineImage(entity, null);
} }
@Override @Override
public void inlineImages(Dashboard dashboard) { public List<TbResourceInfo> inlineImages(Dashboard dashboard) {
log.trace("Executing inlineImage [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId()); log.trace("Executing inlineImage [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
inlineImage(dashboard); List<TbResourceInfo> images = new ArrayList<>();
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration()); inlineImage(dashboard, images);
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), images);
return images;
} }
@Override @Override
public void inlineImages(WidgetTypeDetails widgetTypeDetails) { public List<TbResourceInfo> inlineImages(WidgetTypeDetails widgetTypeDetails) {
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId()); log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
inlineImage(widgetTypeDetails); List<TbResourceInfo> images = new ArrayList<>();
inlineImage(widgetTypeDetails, images);
ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor(); ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor();
inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor); inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor, images);
if (descriptor.has(DEFAULT_CONFIG_TAG) && descriptor.get(DEFAULT_CONFIG_TAG).isTextual()) { if (descriptor.has(DEFAULT_CONFIG_TAG) && descriptor.get(DEFAULT_CONFIG_TAG).isTextual()) {
try { try {
var defaultConfig = JacksonUtil.toJsonNode(descriptor.get(DEFAULT_CONFIG_TAG).asText()); var defaultConfig = JacksonUtil.toJsonNode(descriptor.get(DEFAULT_CONFIG_TAG).asText());
inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig); inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig, images);
descriptor.put(DEFAULT_CONFIG_TAG, JacksonUtil.toString(defaultConfig)); descriptor.put(DEFAULT_CONFIG_TAG, JacksonUtil.toString(defaultConfig));
} catch (Exception e) { } catch (Exception e) {
log.debug("[{}][{}] Failed to process default config: ", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), e); log.debug("[{}][{}] Failed to process default config: ", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), e);
} }
} }
return images;
} }
@Override @Override
public void inlineImageForEdge(HasImage entity) { public void inlineImageForEdge(HasImage entity) {
log.trace("Executing inlineImageForEdge [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName()); log.trace("Executing inlineImageForEdge [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), false)); entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), false, null));
} }
@Override @Override
public void inlineImagesForEdge(Dashboard dashboard) { public void inlineImagesForEdge(Dashboard dashboard) {
log.trace("Executing inlineImagesForEdge [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId()); log.trace("Executing inlineImagesForEdge [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
inlineImageForEdge(dashboard); inlineImageForEdge(dashboard);
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), false); inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), false, null);
} }
@Override @Override
public void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails) { public void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails) {
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId()); log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
inlineImageForEdge(widgetTypeDetails); inlineImageForEdge(widgetTypeDetails);
inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), false); inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), false, null);
} }
private void inlineIntoJson(TenantId tenantId, JsonNode root) { private void inlineImage(HasImage entity, List<TbResourceInfo> processedImages) {
inlineIntoJson(tenantId, root, true); log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), true, processedImages));
} }
private void inlineIntoJson(TenantId tenantId, JsonNode root, boolean addTbImagePrefix) { private void inlineIntoJson(TenantId tenantId, JsonNode root, List<TbResourceInfo> processedImages) {
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>(); inlineIntoJson(tenantId, root, true, processedImages);
tasks.add(new JsonNodeProcessingTask("", root)); }
while (!tasks.isEmpty()) {
JsonNodeProcessingTask task = tasks.poll(); private void inlineIntoJson(TenantId tenantId, JsonNode root, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
JsonNode node = task.getNode(); JacksonUtil.replaceAll(root, "", (path, value) -> inlineImage(tenantId, path, value, addTbImagePrefix, processedImages));
if (node == null) { }
continue;
} private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + "."); return inlineImage(tenantId, path, url, (key, imageInfo) -> {
if (node.isObject()) { String tbImagePrefix = "";
ObjectNode on = (ObjectNode) node; boolean addData = true;
for (Iterator<String> it = on.fieldNames(); it.hasNext(); ) { if (addTbImagePrefix) {
String childName = it.next(); tbImagePrefix = "tb-image:" + encode(imageInfo.getResourceKey()) + ":"
JsonNode childValue = on.get(childName); + encode(imageInfo.getName()) + ":"
if (childValue.isTextual()) { + encode(imageInfo.getResourceSubType().name()) + ":"
on.put(childName, inlineImage(tenantId, currentPath + childName, childValue.asText(), addTbImagePrefix)); + imageInfo.getEtag() + ";";
} else if (childValue.isObject() || childValue.isArray()) {
tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue)); if (processedImages != null && !key.isPreview()) {
} addData = false;
} processedImages.add(imageInfo);
} else if (node.isArray()) {
ArrayNode childArray = (ArrayNode) node;
for (int i = 0; i < childArray.size(); i++) {
JsonNode element = childArray.get(i);
if (element.isObject()) {
tasks.add(new JsonNodeProcessingTask(currentPath + "." + i, element));
} else if (element.isTextual()) {
childArray.set(i, inlineImage(tenantId, currentPath + "." + i, element.asText(), addTbImagePrefix));
}
} }
} }
}
byte[] data;
if (addData) {
data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId());
} else {
data = null;
}
ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + encode(data);
});
} }
private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix) { private String inlineImage(TenantId tenantId, String path, String imageUrl, BiFunction<ImageCacheKey, TbResourceInfo, String> inliner) {
try { try {
ImageCacheKey key = getKeyFromUrl(tenantId, url); ImageCacheKey key = getKeyFromUrl(tenantId, imageUrl);
if (key != null) { if (key != null) {
var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getResourceKey()); var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getResourceKey());
if (imageInfo != null && !(TenantId.SYS_TENANT_ID.equals(imageInfo.getTenantId()) && ResourceSubType.SCADA_SYMBOL.equals(imageInfo.getResourceSubType()))) { if (imageInfo != null && !(TenantId.SYS_TENANT_ID.equals(imageInfo.getTenantId()) && ResourceSubType.SCADA_SYMBOL.equals(imageInfo.getResourceSubType()))) {
byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); return inliner.apply(key, imageInfo);
ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
String tbImagePrefix = "";
if (addTbImagePrefix) {
tbImagePrefix = "tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes(StandardCharsets.UTF_8)) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes(StandardCharsets.UTF_8)) + ";";
}
return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + Base64.getEncoder().encodeToString(data);
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, url, e); log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, imageUrl, e);
} }
return url; return imageUrl;
} }
private ImageDescriptor getImageDescriptor(TbResourceInfo imageInfo, boolean preview) throws JsonProcessingException { private ImageDescriptor getImageDescriptor(TbResourceInfo imageInfo, boolean preview) {
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
return preview ? descriptor.getPreviewDescriptor() : descriptor; return preview ? descriptor.getPreviewDescriptor() : descriptor;
} }
@ -681,6 +662,24 @@ 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;

View File

@ -55,7 +55,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.ImageExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceSubType;
@ -3802,14 +3802,14 @@ public class RestClient implements Closeable {
return IOUtils.toByteArray(image.getInputStream()); return IOUtils.toByteArray(image.getInputStream());
} }
public ImageExportData exportImage(String type, String key) { public ResourceExportData exportImage(String type, String key) {
return restTemplate.getForObject(baseURL + "/api/images/{type}/{key}/export", ImageExportData.class, Map.of( return restTemplate.getForObject(baseURL + "/api/images/{type}/{key}/export", ResourceExportData.class, Map.of(
"type", type, "type", type,
"key", key "key", key
)); ));
} }
public TbResourceInfo importImage(ImageExportData exportData) { public TbResourceInfo importImage(ResourceExportData exportData) {
return restTemplate.exchange(baseURL + "/api/image/import", HttpMethod.PUT, new HttpEntity<>(exportData), TbResourceInfo.class).getBody(); return restTemplate.exchange(baseURL + "/api/image/import", HttpMethod.PUT, new HttpEntity<>(exportData), TbResourceInfo.class).getBody();
} }

View File

@ -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, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models'; import { Dashboard, DashboardExportData, 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<Dashboard> { public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<DashboardExportData> {
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}?inlineImages=true`, defaultHttpOptionsFromConfig(config)); return this.http.get<DashboardExportData>(`/api/dashboard/${dashboardId}/export`, defaultHttpOptionsFromConfig(config));
} }
public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> { public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> {

View File

@ -144,10 +144,12 @@ export class ImportExportService {
public exportDashboard(dashboardId: string) { public exportDashboard(dashboardId: string) {
this.dashboardService.exportDashboard(dashboardId).subscribe({ this.dashboardService.exportDashboard(dashboardId).subscribe({
next: (dashboard) => { next: (exportData) => {
let dashboard = exportData.dashboard;
let name = dashboard.title; let name = dashboard.title;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareDashboardExport(dashboard), name); exportData.dashboard = this.prepareDashboardExport(dashboard)
this.exportToPc(exportData, name);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'dashboard.export-failed-error'); this.handleExportError(e, 'dashboard.export-failed-error');
@ -155,6 +157,7 @@ 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) => {

View File

@ -209,6 +209,11 @@ 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