Change dashboard export structure; images export-import improvements
This commit is contained in:
parent
2a71abbd52
commit
cb097bb637
@ -26,7 +26,10 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
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.Tenant;
|
||||
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.exception.ThingsboardException;
|
||||
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). " +
|
||||
"Used to adjust view of the dashboards according to the difference between browser and server time.")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/dashboard/serverTime")
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137")))
|
||||
public long getServerTime() throws ThingsboardException {
|
||||
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. " +
|
||||
"The actual value of the limit is configurable in the system configuration file.")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/dashboard/maxDatapointsLimit", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/dashboard/maxDatapointsLimit")
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000")))
|
||||
public long getMaxDatapointsLimit() throws ThingsboardException {
|
||||
return maxDatapointsLimit;
|
||||
@ -134,8 +136,7 @@ public class DashboardController extends BaseController {
|
||||
@ApiOperation(value = "Get Dashboard Info (getDashboardInfoById)",
|
||||
notes = "Get the information about the dashboard based on 'dashboardId' parameter. " + DASHBOARD_INFO_DEFINITION)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/dashboard/info/{dashboardId}")
|
||||
public DashboardInfo getDashboardInfoById(
|
||||
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@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
|
||||
)
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/dashboard/{dashboardId}")
|
||||
public Dashboard getDashboardById(
|
||||
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(DASHBOARD_ID) String strDashboardId,
|
||||
@ -164,6 +164,15 @@ public class DashboardController extends BaseController {
|
||||
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)",
|
||||
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. " +
|
||||
@ -172,23 +181,29 @@ public class DashboardController extends BaseController {
|
||||
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " +
|
||||
TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/dashboard", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
public Dashboard saveDashboard(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
|
||||
@PostMapping(value = "/dashboard")
|
||||
public Dashboard saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
|
||||
@RequestBody Dashboard dashboard) throws Exception {
|
||||
dashboard.setTenantId(getTenantId());
|
||||
checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
|
||||
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)",
|
||||
notes = "Delete the Dashboard." + TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.DELETE)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public void deleteDashboard(
|
||||
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@DeleteMapping(value = "/dashboard/{dashboardId}")
|
||||
public void deleteDashboard(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
|
||||
checkParameter(DASHBOARD_ID, strDashboardId);
|
||||
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
|
||||
@ -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. " +
|
||||
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
@PostMapping(value = "/customer/{customerId}/dashboard/{dashboardId}")
|
||||
public Dashboard assignDashboardToCustomer(
|
||||
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
|
||||
@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. " +
|
||||
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.DELETE)
|
||||
@ResponseBody
|
||||
@DeleteMapping(value = "/customer/{customerId}/dashboard/{dashboardId}")
|
||||
public Dashboard unassignDashboardFromCustomer(
|
||||
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(CUSTOMER_ID) String strCustomerId,
|
||||
@ -243,8 +256,7 @@ public class DashboardController extends BaseController {
|
||||
"Returns the Dashboard object. " + TENANT_AUTHORITY_PARAGRAPH)
|
||||
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/dashboard/{dashboardId}/customers", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
@PostMapping(value = "/dashboard/{dashboardId}/customers")
|
||||
public Dashboard updateDashboardCustomers(
|
||||
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@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. " +
|
||||
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/dashboard/{dashboardId}/customers/add", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
@PostMapping(value = "/dashboard/{dashboardId}/customers/add")
|
||||
public Dashboard addDashboardCustomers(
|
||||
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@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. " +
|
||||
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/dashboard/{dashboardId}/customers/remove", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
@PostMapping(value = "/dashboard/{dashboardId}/customers/remove")
|
||||
public Dashboard removeDashboardCustomers(
|
||||
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(DASHBOARD_ID) String strDashboardId,
|
||||
@ -655,4 +665,5 @@ public class DashboardController extends BaseController {
|
||||
}
|
||||
return customerIds;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
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.ResourceType;
|
||||
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.Resource;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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')")
|
||||
@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,
|
||||
@Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true)
|
||||
@PathVariable String key) throws Exception {
|
||||
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
|
||||
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
|
||||
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();
|
||||
return tbImageService.exportImage(imageInfo);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@PutMapping("/api/image/import")
|
||||
public TbResourceInfo importImage(@RequestBody ImageExportData imageData) throws Exception {
|
||||
public TbResourceInfo importImage(@RequestBody ResourceExportData imageData) throws Exception {
|
||||
SecurityUser user = getCurrentUser();
|
||||
TbResource image = new TbResource();
|
||||
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);
|
||||
return tbImageService.importImage(imageData, false, user);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
|
||||
@ -21,7 +21,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
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.security.Authority;
|
||||
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.WidgetTypeDetails;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
|
||||
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.dao.model.ModelConstants;
|
||||
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.Collections;
|
||||
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.INLINE_IMAGES;
|
||||
@ -93,8 +96,7 @@ public class WidgetTypeController extends AutoCommitController {
|
||||
@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)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/widgetType/{widgetTypeId}")
|
||||
public WidgetTypeDetails getWidgetTypeById(
|
||||
@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
|
||||
@PathVariable("widgetTypeId") String strWidgetTypeId,
|
||||
@ -109,6 +111,14 @@ public class WidgetTypeController extends AutoCommitController {
|
||||
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)",
|
||||
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')")
|
||||
@ -151,6 +161,15 @@ public class WidgetTypeController extends AutoCommitController {
|
||||
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)",
|
||||
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')")
|
||||
|
||||
@ -35,6 +35,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
|
||||
import org.thingsboard.server.dao.edge.EdgeService;
|
||||
import org.thingsboard.server.dao.model.ModelConstants;
|
||||
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.telemetry.AlarmSubscriptionService;
|
||||
|
||||
@ -71,6 +72,8 @@ public abstract class AbstractTbEntityService {
|
||||
@Autowired(required = false)
|
||||
@Lazy
|
||||
private EntitiesVersionControlService vcService;
|
||||
@Autowired
|
||||
protected AccessControlService accessControlService;
|
||||
|
||||
protected boolean isTestProfile() {
|
||||
return Set.of(this.env.getActiveProfiles()).contains("test");
|
||||
|
||||
@ -20,9 +20,13 @@ import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.server.common.data.Customer;
|
||||
import org.thingsboard.server.common.data.Dashboard;
|
||||
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.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
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.exception.ThingsboardException;
|
||||
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.TenantId;
|
||||
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.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.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@ -42,6 +52,8 @@ import java.util.Set;
|
||||
public class DefaultTbDashboardService extends AbstractTbEntityService implements TbDashboardService {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
private final ImageService imageService;
|
||||
private final TbImageService tbImageService;
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -18,12 +18,14 @@ package org.thingsboard.server.service.entitiy.dashboard;
|
||||
import org.thingsboard.server.common.data.Customer;
|
||||
import org.thingsboard.server.common.data.Dashboard;
|
||||
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.exception.ThingsboardException;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.DashboardId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ -47,4 +49,8 @@ public interface TbDashboardService extends SimpleTbEntityService<Dashboard> {
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@ -18,15 +18,27 @@ package org.thingsboard.server.service.entitiy.widgets.type;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
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.widget.WidgetExportData;
|
||||
import org.thingsboard.server.common.data.widget.WidgetType;
|
||||
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.queue.util.TbCoreComponent;
|
||||
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
|
||||
@TbCoreComponent
|
||||
@ -35,6 +47,8 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
|
||||
|
||||
|
||||
private final WidgetTypeService widgetTypeService;
|
||||
private final ImageService imageService;
|
||||
private final TbImageService tbImageService;
|
||||
|
||||
@Override
|
||||
public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception {
|
||||
@ -75,4 +89,30 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,11 +16,19 @@
|
||||
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.widget.WidgetExportData;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
|
||||
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
|
||||
public interface TbWidgetTypeService extends SimpleTbEntityService<WidgetTypeDetails> {
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@ -24,7 +24,9 @@ import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.server.cluster.TbClusterService;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
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.TbResource;
|
||||
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.queue.util.TbCoreComponent;
|
||||
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.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@TbCoreComponent
|
||||
@ -89,7 +97,7 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb
|
||||
try {
|
||||
var oldEtag = getEtag(image);
|
||||
TbResourceInfo existingImage = null;
|
||||
if (image.getId() == null && StringUtils.isNotEmpty(image.getResourceKey())) {
|
||||
if (image.getId() == null && isNotEmpty(image.getResourceKey())) {
|
||||
existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey());
|
||||
if (existingImage != null) {
|
||||
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) {
|
||||
toEvict.forEach(this::evictETags);
|
||||
clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder()
|
||||
|
||||
@ -15,11 +15,13 @@
|
||||
*/
|
||||
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.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.dao.resource.ImageCacheKey;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
|
||||
public interface TbImageService {
|
||||
|
||||
@ -35,4 +37,8 @@ public interface TbImageService {
|
||||
|
||||
void evictETags(ImageCacheKey imageCacheKey);
|
||||
|
||||
ResourceExportData exportImage(TbResourceInfo imageInfo);
|
||||
|
||||
TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@ -41,18 +41,22 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.mock.http.MockHttpInputMessage;
|
||||
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.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
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.MultiValueMap;
|
||||
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.SaveDeviceWithCredentialsRequest;
|
||||
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.TenantProfile;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
@ -131,6 +136,7 @@ import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -1162,4 +1168,19 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -26,18 +26,22 @@ import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.Customer;
|
||||
import org.thingsboard.server.common.data.Dashboard;
|
||||
import org.thingsboard.server.common.data.DashboardInfo;
|
||||
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.StringUtils;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.Tenant;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.asset.AssetProfile;
|
||||
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.id.CustomerId;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ -575,7 +580,37 @@ public class DashboardControllerTest extends AbstractControllerTest {
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText();
|
||||
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) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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.widget.WidgetTypeDetails;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ImageService {
|
||||
|
||||
TbResourceInfo saveImage(TbResource image);
|
||||
@ -59,13 +61,14 @@ public interface ImageService {
|
||||
|
||||
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 inlineImagesForEdge(Dashboard dashboard);
|
||||
|
||||
void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails);
|
||||
|
||||
}
|
||||
|
||||
@ -28,15 +28,16 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ImageExportData {
|
||||
public class ResourceExportData {
|
||||
|
||||
private String mediaType;
|
||||
private String fileName;
|
||||
private String title;
|
||||
private String subType;
|
||||
private ResourceType type;
|
||||
private ResourceSubType subType;
|
||||
private String resourceKey;
|
||||
private boolean isPublic;
|
||||
private String fileName;
|
||||
private String publicResourceKey;
|
||||
private boolean isPublic;
|
||||
private String mediaType;
|
||||
private String data;
|
||||
|
||||
}
|
||||
@ -17,11 +17,11 @@ package org.thingsboard.server.common.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -134,11 +134,12 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
descriptor = updater.apply(descriptor);
|
||||
setDescriptorValue(descriptor);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -32,7 +32,9 @@ import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.Data;
|
||||
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.KvEntry;
|
||||
|
||||
@ -46,10 +48,13 @@ import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.resource;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
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.util.ImageUtils;
|
||||
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.widget.WidgetTypeDao;
|
||||
import org.thingsboard.server.dao.widget.WidgetsBundleDao;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -72,8 +71,12 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.apache.commons.lang3.ArrayUtils.get;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class BaseImageService extends BaseResourceService implements ImageService {
|
||||
@ -293,6 +296,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
|
||||
|
||||
@Override
|
||||
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) {
|
||||
if (StringUtils.isEmpty(etag)) {
|
||||
return null;
|
||||
}
|
||||
log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (StringUtils.isBlank(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);
|
||||
boolean matches = matcher.matches();
|
||||
String mdResourceKey = null;
|
||||
String mdResourceName = null;
|
||||
String mdResourceSubType = null;
|
||||
String mdMediaType;
|
||||
if (matches) {
|
||||
mdResourceKey = new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8);
|
||||
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);
|
||||
if (matcher.matches()) {
|
||||
String[] metadata = matcher.group(1).split(":");
|
||||
resourceKey = decode(get(metadata, 0));
|
||||
resourceName = decode(get(metadata, 1));
|
||||
resourceSubType = decode(get(metadata, 2));
|
||||
etag = get(metadata, 3);
|
||||
mediaType = matcher.group(2);
|
||||
} 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 {
|
||||
return UpdateResult.of(false, data);
|
||||
}
|
||||
|
||||
String base64Data = StringUtils.substringAfter(data, "base64,");
|
||||
String extension = ImageUtils.mediaTypeToFileExtension(mdMediaType);
|
||||
byte[] imageData = Base64.getDecoder().decode(base64Data);
|
||||
String etag = calculateEtag(imageData);
|
||||
byte[] imageData = StringUtils.isNotEmpty(base64Data) ? Base64.getDecoder().decode(base64Data) : null;
|
||||
if (StringUtils.isBlank(etag)) {
|
||||
etag = calculateEtag(imageData);
|
||||
}
|
||||
var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag);
|
||||
if (imageInfo == null) {
|
||||
if (imageData == null) {
|
||||
return UpdateResult.of(false, data);
|
||||
}
|
||||
TbResource image = new TbResource();
|
||||
image.setTenantId(tenantId);
|
||||
image.setResourceType(ResourceType.IMAGE);
|
||||
if (StringUtils.isBlank(mdResourceName)) {
|
||||
mdResourceName = name;
|
||||
if (StringUtils.isBlank(resourceName)) {
|
||||
resourceName = name;
|
||||
}
|
||||
image.setTitle(mdResourceName);
|
||||
image.setTitle(resourceName);
|
||||
|
||||
String fileName;
|
||||
if (StringUtils.isBlank(mdResourceKey)) {
|
||||
fileName = StringUtils.strip(mdResourceName.toLowerCase()
|
||||
if (StringUtils.isBlank(resourceKey)) {
|
||||
String extension = ImageUtils.mediaTypeToFileExtension(mediaType);
|
||||
fileName = StringUtils.strip(resourceName.toLowerCase()
|
||||
.replaceAll("['\"]", "")
|
||||
.replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers
|
||||
+ "." + extension;
|
||||
} else {
|
||||
fileName = mdResourceKey;
|
||||
fileName = resourceKey;
|
||||
}
|
||||
if (StringUtils.isBlank(mdResourceSubType)) {
|
||||
if (StringUtils.isBlank(resourceSubType)) {
|
||||
image.setResourceSubType(ResourceSubType.IMAGE);
|
||||
} else {
|
||||
image.setResourceSubType(ResourceSubType.valueOf(mdResourceSubType));
|
||||
image.setResourceSubType(ResourceSubType.valueOf(resourceSubType));
|
||||
}
|
||||
image.setFileName(fileName);
|
||||
image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType));
|
||||
image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType));
|
||||
image.setData(imageData);
|
||||
image.setPublic(true);
|
||||
try {
|
||||
@ -503,159 +516,127 @@ public class BaseImageService extends BaseResourceService implements ImageServic
|
||||
}
|
||||
|
||||
private boolean base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) {
|
||||
boolean updated = false;
|
||||
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>();
|
||||
tasks.add(new JsonNodeProcessingTask(title, root));
|
||||
while (!tasks.isEmpty()) {
|
||||
JsonNodeProcessingTask task = tasks.poll();
|
||||
JsonNode node = task.getNode();
|
||||
if (node == null) {
|
||||
continue;
|
||||
AtomicBoolean updated = new AtomicBoolean(false);
|
||||
JacksonUtil.replaceAll(root, title, (path, value) -> {
|
||||
UpdateResult result = base64ToImageUrl(tenantId, path, value, true);
|
||||
if (result.isUpdated()) {
|
||||
updated.set(true);
|
||||
}
|
||||
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()) {
|
||||
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;
|
||||
return result.getValue();
|
||||
});
|
||||
return updated.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inlineImage(HasImage entity) {
|
||||
log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
|
||||
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), true));
|
||||
inlineImage(entity, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inlineImages(Dashboard dashboard) {
|
||||
public List<TbResourceInfo> inlineImages(Dashboard dashboard) {
|
||||
log.trace("Executing inlineImage [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
|
||||
inlineImage(dashboard);
|
||||
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration());
|
||||
List<TbResourceInfo> images = new ArrayList<>();
|
||||
inlineImage(dashboard, images);
|
||||
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), images);
|
||||
return images;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inlineImages(WidgetTypeDetails widgetTypeDetails) {
|
||||
public List<TbResourceInfo> inlineImages(WidgetTypeDetails widgetTypeDetails) {
|
||||
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
|
||||
inlineImage(widgetTypeDetails);
|
||||
List<TbResourceInfo> images = new ArrayList<>();
|
||||
inlineImage(widgetTypeDetails, images);
|
||||
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()) {
|
||||
try {
|
||||
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));
|
||||
} catch (Exception e) {
|
||||
log.debug("[{}][{}] Failed to process default config: ", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), e);
|
||||
}
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inlineImageForEdge(HasImage entity) {
|
||||
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
|
||||
public void inlineImagesForEdge(Dashboard dashboard) {
|
||||
log.trace("Executing inlineImagesForEdge [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
|
||||
inlineImageForEdge(dashboard);
|
||||
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), false);
|
||||
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails) {
|
||||
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
|
||||
inlineImageForEdge(widgetTypeDetails);
|
||||
inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), false);
|
||||
inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), false, null);
|
||||
}
|
||||
|
||||
private void inlineIntoJson(TenantId tenantId, JsonNode root) {
|
||||
inlineIntoJson(tenantId, root, true);
|
||||
private void inlineImage(HasImage entity, List<TbResourceInfo> processedImages) {
|
||||
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) {
|
||||
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>();
|
||||
tasks.add(new JsonNodeProcessingTask("", 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, inlineImage(tenantId, currentPath + childName, childValue.asText(), addTbImagePrefix));
|
||||
} 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, inlineImage(tenantId, currentPath + "." + i, element.asText(), addTbImagePrefix));
|
||||
}
|
||||
private void inlineIntoJson(TenantId tenantId, JsonNode root, List<TbResourceInfo> processedImages) {
|
||||
inlineIntoJson(tenantId, root, true, processedImages);
|
||||
}
|
||||
|
||||
private void inlineIntoJson(TenantId tenantId, JsonNode root, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
|
||||
JacksonUtil.replaceAll(root, "", (path, value) -> inlineImage(tenantId, path, value, addTbImagePrefix, processedImages));
|
||||
}
|
||||
|
||||
private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
|
||||
return inlineImage(tenantId, path, url, (key, imageInfo) -> {
|
||||
String tbImagePrefix = "";
|
||||
boolean addData = true;
|
||||
if (addTbImagePrefix) {
|
||||
tbImagePrefix = "tb-image:" + encode(imageInfo.getResourceKey()) + ":"
|
||||
+ encode(imageInfo.getName()) + ":"
|
||||
+ encode(imageInfo.getResourceSubType().name()) + ":"
|
||||
+ imageInfo.getEtag() + ";";
|
||||
|
||||
if (processedImages != null && !key.isPreview()) {
|
||||
addData = false;
|
||||
processedImages.add(imageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private String inlineImage(TenantId tenantId, String path, String url, boolean 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 imageUrl, BiFunction<ImageCacheKey, TbResourceInfo, String> inliner) {
|
||||
try {
|
||||
ImageCacheKey key = getKeyFromUrl(tenantId, url);
|
||||
ImageCacheKey key = getKeyFromUrl(tenantId, imageUrl);
|
||||
if (key != null) {
|
||||
var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getResourceKey());
|
||||
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());
|
||||
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);
|
||||
return inliner.apply(key, imageInfo);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
return preview ? descriptor.getPreviewDescriptor() : descriptor;
|
||||
}
|
||||
@ -681,6 +662,24 @@ public class BaseImageService extends BaseResourceService implements ImageServic
|
||||
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")
|
||||
private static class UpdateResult {
|
||||
private final boolean updated;
|
||||
|
||||
@ -55,7 +55,7 @@ import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.EntityView;
|
||||
import org.thingsboard.server.common.data.EntityViewInfo;
|
||||
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.OtaPackageInfo;
|
||||
import org.thingsboard.server.common.data.ResourceSubType;
|
||||
@ -3802,14 +3802,14 @@ public class RestClient implements Closeable {
|
||||
return IOUtils.toByteArray(image.getInputStream());
|
||||
}
|
||||
|
||||
public ImageExportData exportImage(String type, String key) {
|
||||
return restTemplate.getForObject(baseURL + "/api/images/{type}/{key}/export", ImageExportData.class, Map.of(
|
||||
public ResourceExportData exportImage(String type, String key) {
|
||||
return restTemplate.getForObject(baseURL + "/api/images/{type}/{key}/export", ResourceExportData.class, Map.of(
|
||||
"type", type,
|
||||
"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();
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { PageLink } from '@shared/models/page/page-link';
|
||||
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 { NavigationEnd, Router } from '@angular/router';
|
||||
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));
|
||||
}
|
||||
|
||||
public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<Dashboard> {
|
||||
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}?inlineImages=true`, defaultHttpOptionsFromConfig(config));
|
||||
public exportDashboard(dashboardId: string, config?: RequestConfig): Observable<DashboardExportData> {
|
||||
return this.http.get<DashboardExportData>(`/api/dashboard/${dashboardId}/export`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getDashboardInfo(dashboardId: string, config?: RequestConfig): Observable<DashboardInfo> {
|
||||
|
||||
@ -144,10 +144,12 @@ export class ImportExportService {
|
||||
|
||||
public exportDashboard(dashboardId: string) {
|
||||
this.dashboardService.exportDashboard(dashboardId).subscribe({
|
||||
next: (dashboard) => {
|
||||
next: (exportData) => {
|
||||
let dashboard = exportData.dashboard;
|
||||
let name = dashboard.title;
|
||||
name = name.toLowerCase().replace(/\W/g, '_');
|
||||
this.exportToPc(this.prepareDashboardExport(dashboard), name);
|
||||
exportData.dashboard = this.prepareDashboardExport(dashboard)
|
||||
this.exportToPc(exportData, name);
|
||||
},
|
||||
error: (e) => {
|
||||
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> {
|
||||
return this.openImportDialog('dashboard.import', 'dashboard.dashboard-file').pipe(
|
||||
mergeMap((dashboard: Dashboard) => {
|
||||
|
||||
@ -209,6 +209,11 @@ export interface DashboardSetup extends Dashboard {
|
||||
assignedCustomerIds?: Array<string>;
|
||||
}
|
||||
|
||||
export interface DashboardExportData {
|
||||
dashboard?: Dashboard;
|
||||
resources: any;
|
||||
}
|
||||
|
||||
export const isPublicDashboard = (dashboard: DashboardInfo): boolean => {
|
||||
if (dashboard && dashboard.assignedCustomers) {
|
||||
return dashboard.assignedCustomers
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user