From 9889dc148f423c170f9d08fbfabb00c16d49b2f3 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 3 Nov 2023 13:09:57 +0200 Subject: [PATCH] Upgrade scripts for migrating to image resources --- .../install/ThingsboardInstallService.java | 1 + .../service/install/InstallScripts.java | 61 +- .../service/install/update/ImagesUpdater.java | 180 ++++++ .../service/install/InstallScriptsTest.java | 536 ++++++++++++++++++ .../dao/dashboard/DashboardService.java | 2 + .../server/dao/resource/ResourceService.java | 2 + .../common/data/util/MediaTypeUtils.java | 22 + .../server/dao/dashboard/DashboardDao.java | 2 + .../dao/dashboard/DashboardServiceImpl.java | 5 + .../dao/resource/BaseResourceService.java | 7 + .../dao/resource/TbResourceInfoDao.java | 4 + .../validator/ResourceDataValidator.java | 3 +- .../sql/dashboard/DashboardRepository.java | 3 + .../dao/sql/dashboard/JpaDashboardDao.java | 5 + .../sql/resource/JpaTbResourceInfoDao.java | 6 + .../resource/TbResourceInfoRepository.java | 3 + 16 files changed, 825 insertions(+), 17 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/util/MediaTypeUtils.java diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 773174d651..f37d1f37bc 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -280,6 +280,7 @@ public class ThingsboardInstallService { log.info("Updating system data..."); dataUpdateService.upgradeRuleNodes(); systemDataLoaderService.updateSystemWidgets(); + installScripts.updateDashboards(); // fixme: can't work properly if widgets not updated first installScripts.loadSystemLwm2mResources(); } log.info("Upgrade finished successfully!"); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 49f487ebf0..62232a1637 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -17,18 +17,20 @@ package org.thingsboard.server.service.install; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; 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.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; @@ -40,6 +42,7 @@ import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.service.install.update.ImagesUpdater; import java.io.IOException; import java.nio.file.DirectoryStream; @@ -102,6 +105,9 @@ public class InstallScripts { @Autowired private ResourceService resourceService; + @Autowired + private ImagesUpdater imagesUpdater; + Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } @@ -146,16 +152,14 @@ public class InstallScripts { } private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { - findRuleChainsFromPath(ruleChainsPath) - .forEach( - path -> { - try { - createRuleChainFromFile(tenantId, path, null); - } catch (Exception e) { - log.error("Unable to load rule chain from json: [{}]", path.toString()); - throw new RuntimeException("Unable to load rule chain from json", e); - } - }); + findRuleChainsFromPath(ruleChainsPath).forEach(path -> { + try { + createRuleChainFromFile(tenantId, path, null); + } catch (Exception e) { + log.error("Unable to load rule chain from json: [{}]", path.toString()); + throw new RuntimeException("Unable to load rule chain from json", e); + } + }); } List findRuleChainsFromPath(Path ruleChainsPath) throws IOException { @@ -188,6 +192,7 @@ public class InstallScripts { } public void loadSystemWidgets() throws Exception { + log.info("Loading system widgets"); Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); if (Files.exists(widgetTypesDir)) { try (DirectoryStream dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) { @@ -195,8 +200,7 @@ public class InstallScripts { path -> { try { JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile()); - WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); - widgetTypeService.saveWidgetType(widgetTypeDetails); + saveWidgetType(widgetTypeJson); } catch (Exception e) { log.error("Unable to load widget type from json: [{}]", path.toString()); throw new RuntimeException("Unable to load widget type from json", e); @@ -213,6 +217,7 @@ public class InstallScripts { JsonNode widgetsBundleDescriptorJson = JacksonUtil.toJsonNode(path.toFile()); JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle"); WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class); + imagesUpdater.updateWidgetsBundleImages(widgetsBundle); WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); List widgetTypeFqns = new ArrayList<>(); if (widgetsBundleDescriptorJson.has("widgetTypes")) { @@ -220,8 +225,7 @@ public class InstallScripts { widgetTypesArrayJson.forEach( widgetTypeJson -> { try { - WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); - var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails); + var savedWidgetType = saveWidgetType(widgetTypeJson); widgetTypeFqns.add(savedWidgetType.getFqn()); } catch (Exception e) { log.error("Unable to load widget type from json: [{}]", path.toString()); @@ -246,6 +250,32 @@ public class InstallScripts { } } + private WidgetTypeDetails saveWidgetType(JsonNode widgetTypeJson) { + WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); + try { + imagesUpdater.updateWidgetImages(widgetTypeDetails); + } catch (Exception e) { + log.warn("Failed to process images for widget type {}", widgetTypeDetails.getName(), e); + } + return widgetTypeService.saveWidgetType(widgetTypeDetails); + } + + public void updateDashboards() { + var dashboards = new PageDataIterable<>(dashboardService::findAllDashboardsIds, 1024); + for (DashboardId dashboardId : dashboards) { + Dashboard dashboard = dashboardService.findDashboardById(TenantId.SYS_TENANT_ID, dashboardId); + log.debug("Updating images for dashboard '{}' ({})", dashboard.getTitle(), dashboardId); + try { + boolean updated = imagesUpdater.updateDashboardImages(dashboard); + if (updated) { + dashboardService.saveDashboard(dashboard); + } + } catch (Exception e) { + log.error("Failed to update images for dashboard '{}' ({})", dashboard.getTitle(), dashboardId, e); + } + } + } + public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { @@ -339,4 +369,3 @@ public class InstallScripts { } } } - diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java new file mode 100644 index 0000000000..051c4eb054 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java @@ -0,0 +1,180 @@ +package org.thingsboard.server.service.install.update; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.util.MediaTypeUtils; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.resource.ResourceService; + +import java.util.List; +import java.util.Optional; + +@Component +//@Profile("install") +@Slf4j +@RequiredArgsConstructor +public class ImagesUpdater { + + private final ResourceService resourceService; + + + // TODO: solution templates + + + public void updateWidgetImages(WidgetTypeDetails widgetTypeDetails) { + String imageName = widgetTypeDetails.getName() + " - image"; + String imageKey = widgetTypeDetails.getFqn(); + String imageLink = saveImage(imageName, imageKey, widgetTypeDetails.getImage()); + widgetTypeDetails.setImage(imageLink); + + JsonNode defaultConfig = JacksonUtil.toJsonNode(widgetTypeDetails.getDescriptor().get("defaultConfig").asText()); + defaultConfig = updateWidgetConfig(TenantId.SYS_TENANT_ID, defaultConfig, imageName, imageKey, imageKey); + ((ObjectNode) widgetTypeDetails.getDescriptor()).put("defaultConfig", defaultConfig.toString()); + } + + public void updateWidgetsBundleImages(WidgetsBundle widgetsBundle) { + String imageLink = saveImage(widgetsBundle.getTitle(), widgetsBundle.getAlias(), widgetsBundle.getImage()); + widgetsBundle.setImage(imageLink); + } + + public boolean updateDashboardImages(Dashboard dashboard) { + String imageNamePrefix = dashboard.getTitle(); + String imageKeyPrefix = "dashboard_" + dashboard.getUuidId(); + boolean updated = false; + + String imageLink = saveImage(dashboard.getTenantId(), imageNamePrefix + " - image", imageKeyPrefix + ".image", dashboard.getImage(), null); + dashboard.setImage(imageLink); + + for (ObjectNode widgetConfig : dashboard.getWidgetsConfig()) { + String alias = widgetConfig.get("bundleAlias").asText() + "." + widgetConfig.get("typeAlias").asText(); + String widgetName = widgetConfig.get("config").get("title").asText(); + updateWidgetConfig(dashboard.getTenantId(), widgetConfig.get("config"), + imageNamePrefix + " - " + widgetName + " widget", + imageKeyPrefix + ".widget." + alias, alias); + } + + return updated; + } + + private JsonNode updateWidgetConfig(TenantId tenantId, JsonNode widgetConfig, + String imageNamePrefix, String imageKeyPrefix, + String fqn) { + ObjectNode widgetSettings = (ObjectNode) widgetConfig.get("settings"); + ArrayNode markerImages = (ArrayNode) widgetSettings.get("markerImages"); + if (markerImages != null && !markerImages.isEmpty()) { + for (int i = 0; i < markerImages.size(); i++) { + String imageName = imageNamePrefix + " - marker image " + (i + 1); + String imageKey = imageKeyPrefix + ".marker_image_" + (i + 1); + String imageLink = saveImage(tenantId, imageName, imageKey, markerImages.get(i).asText(), fqn + ".marker_image_"); + markerImages.set(i, imageLink); + } + } + + String mapImage = getText(widgetSettings, "mapImageUrl"); + if (mapImage != null) { + String imageName = imageNamePrefix + " - map image"; + String imageKeySuffix = ".map_image"; + String imageKey = imageKeyPrefix + imageKeySuffix; + String imageLink = saveImage(tenantId, imageName, imageKey, mapImage, fqn + imageKeySuffix); + widgetSettings.put("mapImageUrl", imageLink); + } + + String backgroundImage = getText(widgetSettings, "backgroundImageUrl"); + if (backgroundImage != null) { + String imageName = imageNamePrefix + " - background image"; + String imageKeySuffix = ".background_image"; + String imageKey = imageKeyPrefix + imageKeySuffix; + String imageLink = saveImage(tenantId, imageName, imageKey, backgroundImage, fqn + imageKeySuffix); + widgetSettings.put("backgroundImageUrl", imageLink); + } + + JsonNode backgroundConfigNode = widgetSettings.get("background"); + if (backgroundConfigNode != null && backgroundConfigNode.isObject()) { + ObjectNode backgroundConfig = (ObjectNode) backgroundConfigNode; + if ("image".equals(getText(backgroundConfig, "type"))) { + String imageBase64 = getText(backgroundConfig, "imageBase64"); + if (imageBase64 != null) { + String imageName = imageNamePrefix + " - background image"; + String imageKeySuffix = ".background_image"; + String imageKey = imageKeyPrefix + imageKeySuffix; + String imageLink = saveImage(tenantId, imageName, imageKey, imageBase64, fqn + imageKeySuffix); + backgroundConfig.set("imageBase64", null); + backgroundConfig.put("imageUrl", imageLink); + backgroundConfig.put("type", "imageUrl"); + } + } + } + + return widgetConfig; + } + + private String getText(JsonNode jsonNode, String field) { + return Optional.ofNullable(jsonNode.get(field)) + .filter(JsonNode::isTextual) + .map(JsonNode::asText).orElse(null); + } + + private String saveImage(String title, String key, String data) { + return saveImage(TenantId.SYS_TENANT_ID, title, key, data, null); + } + + private String saveImage(TenantId tenantId, String title, String key, String data, + String existingImageQuery) { + if (data == null) { + return null; + } + String base64Data = StringUtils.substringAfter(data, "base64,"); + if (base64Data.isEmpty()) { + return data; + } + String imageMediaType = StringUtils.substringBetween(data, "data:", ";base64"); + String extension = MediaTypeUtils.getFileExtension(imageMediaType); + key += "." + extension; + + TbResourceInfo resourceInfo = resourceService.findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key); + if (resourceInfo == null && !tenantId.isSysTenantId() && existingImageQuery != null) { + List existing = resourceService.findByTenantIdAndDataAndKeyStartingWith(TenantId.SYS_TENANT_ID, base64Data, existingImageQuery); + if (!existing.isEmpty()) { + resourceInfo = existing.get(0); + if (existing.size() > 1) { + log.warn("Found more than one system image resources for key {}", existingImageQuery); + } + log.info("Using system image {} for {}", resourceInfo.getLink(), key); + return resourceInfo.getLink(); + } + } + TbResource resource; + if (resourceInfo == null) { + resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.IMAGE); + resource.setResourceKey(key); + } else if (tenantId.isSysTenantId()) { + resource = new TbResource(resourceInfo); + } else { + return resourceInfo.getLink(); + } + resource.setTitle(title); + resource.setFileName(key); + resource.setMediaType(imageMediaType); + resource.setBase64Data(base64Data); + resource = resourceService.saveResource(resource); + log.info("[{}] {} image '{}' {} ({})", tenantId, resourceInfo == null ? "Created" : "Updated", + resource.getTitle(), resource.getResourceKey(), resource.getLink()); + return resource.getLink(); + + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java index 26eead0ec7..af9ec8267d 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java @@ -23,6 +23,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; @@ -36,6 +39,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.service.install.update.ImagesUpdater; import java.io.IOException; import java.nio.file.Path; @@ -46,6 +50,8 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @Slf4j @SpringBootTest(classes = {InstallScripts.class, RuleChainDataValidator.class}) @@ -72,12 +78,27 @@ class InstallScriptsTest { ApiLimitService apiLimitService; @SpyBean RuleChainDataValidator ruleChainValidator; + @SpyBean + ImagesUpdater imagesUpdater; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); @BeforeEach void setUp() { willReturn(true).given(tenantService).tenantExists(tenantId); willReturn(true).given(apiLimitService).checkEntitiesLimit(any(), any()); + + when(resourceService.saveResource(any())).thenAnswer(inv -> { + TbResource resource = inv.getArgument(0); + if (resource.getResourceType() == ResourceType.IMAGE) { + resource.setLink(String.format("/api/images/%s/%s", + resource.getTenantId().isSysTenantId() ? + "system" : resource.getTenantId().toString(), + resource.getResourceKey() + )); + } + return resource; + }); } @Test @@ -116,4 +137,519 @@ class InstallScriptsTest { .containsExactlyInAnyOrderElementsOf(Collections.emptyList()); } + @Test + public void testWidgetsUpdate() throws Exception { + installScripts.loadSystemWidgets(); + } + + @Test + public void testImagesUpdater() { + ImagesUpdater imagesUpdater = new ImagesUpdater(resourceService); + Dashboard dashboard = new Dashboard(); + dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardConfig)); + dashboard.setTenantId(tenantId); + imagesUpdater.updateDashboardImages(dashboard); + System.err.println("Updated config: " + dashboard.getConfiguration().toPrettyString()); + } + + + public static final String dashboardConfig = "{\n" + + " \"description\": \"\",\n" + + " \"widgets\": {\n" + + " \"24b26b1e-bdd0-8b2b-2a96-be150b21ae5e\": {\n" + + " \"typeFullFqn\": \"system.cards.value_card\",\n" + + " \"type\": \"latest\",\n" + + " \"sizeX\": 3,\n" + + " \"sizeY\": 3,\n" + + " \"config\": {\n" + + " \"datasources\": [\n" + + " {\n" + + " \"type\": \"device\",\n" + + " \"name\": \"\",\n" + + " \"deviceId\": \"09bfc8f0-b376-11ed-af9a-5f9d1fc4febb\",\n" + + " \"dataKeys\": [\n" + + " {\n" + + " \"name\": \"temperature\",\n" + + " \"type\": \"timeseries\",\n" + + " \"label\": \"Temperature\",\n" + + " \"color\": \"#2196f3\",\n" + + " \"settings\": {},\n" + + " \"_hash\": 0.790442736163091\n" + + " }\n" + + " ],\n" + + " \"alarmFilterConfig\": {\n" + + " \"statusList\": [\n" + + " \"ACTIVE\"\n" + + " ]\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"timewindow\": {\n" + + " \"displayValue\": \"\",\n" + + " \"selectedTab\": 0,\n" + + " \"realtime\": {\n" + + " \"realtimeType\": 1,\n" + + " \"interval\": 1000,\n" + + " \"timewindowMs\": 60000,\n" + + " \"quickInterval\": \"CURRENT_DAY\"\n" + + " },\n" + + " \"history\": {\n" + + " \"historyType\": 0,\n" + + " \"interval\": 1000,\n" + + " \"timewindowMs\": 60000,\n" + + " \"fixedTimewindow\": {\n" + + " \"startTimeMs\": 1698052629317,\n" + + " \"endTimeMs\": 1698139029317\n" + + " },\n" + + " \"quickInterval\": \"CURRENT_DAY\"\n" + + " },\n" + + " \"aggregation\": {\n" + + " \"type\": \"AVG\",\n" + + " \"limit\": 25000\n" + + " }\n" + + " },\n" + + " \"showTitle\": false,\n" + + " \"backgroundColor\": \"rgba(0, 0, 0, 0)\",\n" + + " \"color\": \"rgba(0, 0, 0, 0.87)\",\n" + + " \"padding\": \"0px\",\n" + + " \"settings\": {\n" + + " \"labelPosition\": \"top\",\n" + + " \"layout\": \"square\",\n" + + " \"showLabel\": true,\n" + + " \"labelFont\": {\n" + + " \"family\": \"Roboto\",\n" + + " \"size\": 16,\n" + + " \"sizeUnit\": \"px\",\n" + + " \"style\": \"normal\",\n" + + " \"weight\": \"500\"\n" + + " },\n" + + " \"labelColor\": {\n" + + " \"type\": \"constant\",\n" + + " \"color\": \"rgba(0, 0, 0, 0.87)\",\n" + + " \"colorFunction\": \"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"\n" + + " },\n" + + " \"showIcon\": true,\n" + + " \"iconSize\": 40,\n" + + " \"iconSizeUnit\": \"px\",\n" + + " \"icon\": \"thermostat\",\n" + + " \"iconColor\": {\n" + + " \"type\": \"constant\",\n" + + " \"color\": \"#5469FF\",\n" + + " \"colorFunction\": \"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"\n" + + " },\n" + + " \"valueFont\": {\n" + + " \"family\": \"Roboto\",\n" + + " \"size\": 52,\n" + + " \"sizeUnit\": \"px\",\n" + + " \"style\": \"normal\",\n" + + " \"weight\": \"500\"\n" + + " },\n" + + " \"valueColor\": {\n" + + " \"type\": \"constant\",\n" + + " \"color\": \"rgba(0, 0, 0, 0.87)\",\n" + + " \"colorFunction\": \"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"\n" + + " },\n" + + " \"showDate\": true,\n" + + " \"dateFormat\": {\n" + + " \"format\": null,\n" + + " \"lastUpdateAgo\": true,\n" + + " \"custom\": false\n" + + " },\n" + + " \"dateFont\": {\n" + + " \"family\": \"Roboto\",\n" + + " \"size\": 12,\n" + + " \"sizeUnit\": \"px\",\n" + + " \"style\": \"normal\",\n" + + " \"weight\": \"500\"\n" + + " },\n" + + " \"dateColor\": {\n" + + " \"type\": \"constant\",\n" + + " \"color\": \"rgba(0, 0, 0, 0.38)\",\n" + + " \"colorFunction\": \"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"\n" + + " },\n" + + " \"background\": {\n" + + " \"type\": \"image\",\n" + + " \"imageBase64\": \"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wCEAAQEBAQEBAUFBQUHBwYHBwoJCAgJCg8KCwoLCg8WDhAODhAOFhQYExITGBQjHBgYHCMpIiAiKTEsLDE+Oz5RUW0BBAQEBAQEBQUFBQcHBgcHCgkICAkKDwoLCgsKDxYOEA4OEA4WFBgTEhMYFCMcGBgcIykiICIpMSwsMT47PlFRbf/CABEIAtAEsAMBEQACEQEDEQH/xAA2AAEAAgIDAQEAAAAAAAAAAAAABQYEBwIDCAEJAQEAAwEBAQEAAAAAAAAAAAAAAQIDBAUGB//aAAwDAQACEAMQAAAA9/AAAAAAAXwsX42s7RHUMhntDzk8zlsOrsWZoeolQ8zbG7KF+Bf/Z\",\n" + + " \"imageUrl\": \"http://localhost:8080/api/resource/myimage\",\n" + + " \"color\": \"#fff\",\n" + + " \"overlay\": {\n" + + " \"enabled\": false,\n" + + " \"color\": \"rgba(255,255,255,0.72)\",\n" + + " \"blur\": 3\n" + + " }\n" + + " },\n" + + " \"autoScale\": true\n" + + " },\n" + + " \"title\": \"Value card\",\n" + + " \"dropShadow\": true,\n" + + " \"enableFullscreen\": false,\n" + + " \"titleStyle\": {\n" + + " \"fontSize\": \"16px\",\n" + + " \"fontWeight\": 400\n" + + " },\n" + + " \"units\": \"°C\",\n" + + " \"decimals\": 0,\n" + + " \"useDashboardTimewindow\": true,\n" + + " \"showLegend\": false,\n" + + " \"widgetStyle\": {},\n" + + " \"actions\": {},\n" + + " \"configMode\": \"basic\",\n" + + " \"displayTimewindow\": true,\n" + + " \"margin\": \"0px\",\n" + + " \"borderRadius\": \"0px\",\n" + + " \"widgetCss\": \"\",\n" + + " \"pageSize\": 1024,\n" + + " \"noDataDisplayMessage\": \"\",\n" + + " \"showTitleIcon\": false,\n" + + " \"titleTooltip\": \"\",\n" + + " \"titleFont\": {\n" + + " \"size\": 12,\n" + + " \"sizeUnit\": \"px\",\n" + + " \"family\": null,\n" + + " \"weight\": null,\n" + + " \"style\": null,\n" + + " \"lineHeight\": \"1.6\"\n" + + " },\n" + + " \"titleIcon\": \"\",\n" + + " \"iconColor\": \"rgba(0, 0, 0, 0.87)\",\n" + + " \"iconSize\": \"14px\",\n" + + " \"timewindowStyle\": {\n" + + " \"showIcon\": true,\n" + + " \"iconSize\": \"14px\",\n" + + " \"icon\": \"query_builder\",\n" + + " \"iconPosition\": \"left\",\n" + + " \"font\": {\n" + + " \"size\": 12,\n" + + " \"sizeUnit\": \"px\",\n" + + " \"family\": null,\n" + + " \"weight\": null,\n" + + " \"style\": null,\n" + + " \"lineHeight\": \"1\"\n" + + " },\n" + + " \"color\": null\n" + + " }\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"24b26b1e-bdd0-8b2b-2a96-be150b21ae5e\"\n" + + " },\n" + + " \"01ad2980-87c8-5813-18a2-47833d8f6df7\": {\n" + + " \"typeFullFqn\": \"system.date.date_range_navigator\",\n" + + " \"type\": \"static\",\n" + + " \"sizeX\": 5,\n" + + " \"sizeY\": 5.5,\n" + + " \"config\": {\n" + + " \"datasources\": [\n" + + " {\n" + + " \"type\": \"static\",\n" + + " \"name\": \"function\",\n" + + " \"dataKeys\": [\n" + + " {\n" + + " \"name\": \"f(x)\",\n" + + " \"type\": \"function\",\n" + + " \"label\": \"Random\",\n" + + " \"color\": \"#2196f3\",\n" + + " \"settings\": {},\n" + + " \"_hash\": 0.15479322438769105,\n" + + " \"funcBody\": \"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"timewindow\": {\n" + + " \"realtime\": {\n" + + " \"timewindowMs\": 60000\n" + + " }\n" + + " },\n" + + " \"showTitle\": true,\n" + + " \"backgroundColor\": \"#C32F2F\",\n" + + " \"color\": \"rgba(0, 0, 0, 0.87)\",\n" + + " \"padding\": \"8px\",\n" + + " \"settings\": {\n" + + " \"defaultInterval\": \"week\",\n" + + " \"stepSize\": \"day\",\n" + + " \"useSessionStorage\": true\n" + + " },\n" + + " \"title\": \"Date-range-navigator\",\n" + + " \"dropShadow\": true,\n" + + " \"enableFullscreen\": true,\n" + + " \"widgetStyle\": {},\n" + + " \"titleStyle\": {\n" + + " \"fontSize\": \"16px\",\n" + + " \"fontWeight\": 400\n" + + " },\n" + + " \"useDashboardTimewindow\": true,\n" + + " \"showLegend\": false,\n" + + " \"actions\": {},\n" + + " \"showTitleIcon\": false,\n" + + " \"titleTooltip\": \"\",\n" + + " \"widgetCss\": \"\",\n" + + " \"pageSize\": 1024,\n" + + " \"noDataDisplayMessage\": \"\"\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"01ad2980-87c8-5813-18a2-47833d8f6df7\"\n" + + " },\n" + + " \"6dc68681-97fe-dca5-5df6-b5006797c9eb\": {\n" + + " \"typeFullFqn\": \"system.maps_v2.image_map\",\n" + + " \"type\": \"latest\",\n" + + " \"sizeX\": 8.5,\n" + + " \"sizeY\": 6.5,\n" + + " \"config\": {\n" + + " \"datasources\": [\n" + + " {\n" + + " \"type\": \"entity\",\n" + + " \"name\": \"\",\n" + + " \"entityAliasId\": \"50e40fe7-fd33-e93e-88f6-212acfa1b311\",\n" + + " \"filterId\": null,\n" + + " \"dataKeys\": [\n" + + " {\n" + + " \"name\": \"pressure\",\n" + + " \"type\": \"timeseries\",\n" + + " \"label\": \"pressure\",\n" + + " \"color\": \"#2196f3\",\n" + + " \"settings\": {},\n" + + " \"_hash\": 0.38936754782620264\n" + + " }\n" + + " ],\n" + + " \"alarmFilterConfig\": {\n" + + " \"statusList\": [\n" + + " \"ACTIVE\"\n" + + " ]\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"timewindow\": {\n" + + " \"displayValue\": \"\",\n" + + " \"selectedTab\": 0,\n" + + " \"realtime\": {\n" + + " \"realtimeType\": 1,\n" + + " \"interval\": 1000,\n" + + " \"timewindowMs\": 60000,\n" + + " \"quickInterval\": \"CURRENT_DAY\"\n" + + " },\n" + + " \"history\": {\n" + + " \"historyType\": 0,\n" + + " \"interval\": 1000,\n" + + " \"timewindowMs\": 60000,\n" + + " \"fixedTimewindow\": {\n" + + " \"startTimeMs\": 1698064891779,\n" + + " \"endTimeMs\": 1698151291779\n" + + " },\n" + + " \"quickInterval\": \"CURRENT_DAY\"\n" + + " },\n" + + " \"aggregation\": {\n" + + " \"type\": \"AVG\",\n" + + " \"limit\": 25000\n" + + " }\n" + + " },\n" + + " \"showTitle\": true,\n" + + " \"backgroundColor\": \"#fff\",\n" + + " \"color\": \"rgba(0, 0, 0, 0.87)\",\n" + + " \"padding\": \"8px\",\n" + + " \"settings\": {\n" + + " \"provider\": \"image-map\",\n" + + " \"gmApiKey\": \"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\n" + + " \"gmDefaultMapType\": \"roadmap\",\n" + + " \"mapProvider\": \"OpenStreetMap.Mapnik\",\n" + + " \"useCustomProvider\": false,\n" + + " \"customProviderTileUrl\": \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\n" + + " \"mapProviderHere\": \"HERE.normalDay\",\n" + + " \"credentials\": {\n" + + " \"useV3\": true,\n" + + " \"app_id\": \"AhM6TzD9ThyK78CT3ptx\",\n" + + " \"app_code\": \"p6NPiITB3Vv0GMUFnkLOOg\",\n" + + " \"apiKey\": \"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"\n" + + " },\n" + + " \"mapImageUrl\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyIAAAKqCAYAAADG2epfAAAABHNCSVQICAgIfAhkiAAAIABJREFUeJzs3Xd4VFX+x/H3nZ6e0ORK5CYII=\",\n" + + " \"tmApiKey\": \"84d6d83e0e51e481e50454ccbe8986b\",\n" + + " \"tmDefaultMapType\": \"roadmap\",\n" + + " \"latKeyName\": \"latitude\",\n" + + " \"lngKeyName\": \"longitude\",\n" + + " \"xPosKeyName\": \"xPos\",\n" + + " \"yPosKeyName\": \"yPos\",\n" + + " \"defaultCenterPosition\": \"0,0\",\n" + + " \"disableScrollZooming\": false,\n" + + " \"disableDoubleClickZooming\": false,\n" + + " \"disableZoomControl\": false,\n" + + " \"fitMapBounds\": true,\n" + + " \"useDefaultCenterPosition\": false,\n" + + " \"mapPageSize\": 16384,\n" + + " \"markerOffsetX\": 0.5,\n" + + " \"markerOffsetY\": 1,\n" + + " \"posFunction\": \"return {x: origXPos, y: origYPos};\",\n" + + " \"draggableMarker\": false,\n" + + " \"showLabel\": true,\n" + + " \"useLabelFunction\": false,\n" + + " \"label\": \"${entityName}\",\n" + + " \"showTooltip\": true,\n" + + " \"showTooltipAction\": \"click\",\n" + + " \"autocloseTooltip\": true,\n" + + " \"useTooltipFunction\": false,\n" + + " \"tooltipPattern\": \"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\n" + + " \"tooltipOffsetX\": 0,\n" + + " \"tooltipOffsetY\": -1,\n" + + " \"color\": \"#fe7569\",\n" + + " \"useColorFunction\": true,\n" + + " \"colorFunction\": \"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\n" + + " \"useMarkerImageFunction\": true,\n" + + " \"markerImageSize\": 34,\n" + + " \"markerImageFunction\": \"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\n" + + " \"markerImages\": [\n" + + " \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgd2w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\n" + + " \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSOFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\n" + + " \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdg7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\n" + + " \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdJRU5ErkJggg==\"\n" + + " ],\n" + + " \"showPolygon\": false,\n" + + " \"polygonKeyName\": \"perimeter\",\n" + + " \"editablePolygon\": false,\n" + + " \"showPolygonLabel\": false,\n" + + " \"usePolygonLabelFunction\": false,\n" + + " \"polygonLabel\": \"${entityName}\",\n" + + " \"showPolygonTooltip\": false,\n" + + " \"showPolygonTooltipAction\": \"click\",\n" + + " \"autoClosePolygonTooltip\": true,\n" + + " \"usePolygonTooltipFunction\": false,\n" + + " \"polygonTooltipPattern\": \"${entityName}

TimeStamp: ${ts:7}\",\n" + + " \"polygonColor\": \"#3388ff\",\n" + + " \"polygonOpacity\": 0.2,\n" + + " \"usePolygonColorFunction\": false,\n" + + " \"polygonStrokeColor\": \"#3388ff\",\n" + + " \"polygonStrokeOpacity\": 1,\n" + + " \"polygonStrokeWeight\": 3,\n" + + " \"usePolygonStrokeColorFunction\": false,\n" + + " \"showCircle\": false,\n" + + " \"circleKeyName\": \"perimeter\",\n" + + " \"editableCircle\": false,\n" + + " \"showCircleLabel\": false,\n" + + " \"useCircleLabelFunction\": false,\n" + + " \"circleLabel\": \"${entityName}\",\n" + + " \"showCircleTooltip\": false,\n" + + " \"showCircleTooltipAction\": \"click\",\n" + + " \"autoCloseCircleTooltip\": true,\n" + + " \"useCircleTooltipFunction\": false,\n" + + " \"circleTooltipPattern\": \"${entityName}

TimeStamp: ${ts:7}\",\n" + + " \"circleFillColor\": \"#3388ff\",\n" + + " \"circleFillColorOpacity\": 0.2,\n" + + " \"useCircleFillColorFunction\": false,\n" + + " \"circleStrokeColor\": \"#3388ff\",\n" + + " \"circleStrokeOpacity\": 1,\n" + + " \"circleStrokeWeight\": 3,\n" + + " \"useCircleStrokeColorFunction\": false\n" + + " },\n" + + " \"title\": \"Image Map\",\n" + + " \"dropShadow\": true,\n" + + " \"enableFullscreen\": true,\n" + + " \"titleStyle\": {\n" + + " \"fontSize\": \"16px\",\n" + + " \"fontWeight\": 400\n" + + " },\n" + + " \"useDashboardTimewindow\": true,\n" + + " \"showLegend\": false,\n" + + " \"widgetStyle\": {},\n" + + " \"actions\": {},\n" + + " \"displayTimewindow\": true\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"6dc68681-97fe-dca5-5df6-b5006797c9eb\"\n" + + " }\n" + + " },\n" + + " \"states\": {\n" + + " \"default\": {\n" + + " \"name\": \"Images\",\n" + + " \"root\": true,\n" + + " \"layouts\": {\n" + + " \"main\": {\n" + + " \"widgets\": {\n" + + " \"24b26b1e-bdd0-8b2b-2a96-be150b21ae5e\": {\n" + + " \"sizeX\": 6,\n" + + " \"sizeY\": 5,\n" + + " \"row\": 0,\n" + + " \"col\": 18\n" + + " },\n" + + " \"01ad2980-87c8-5813-18a2-47833d8f6df7\": {\n" + + " \"sizeX\": 5,\n" + + " \"sizeY\": 5,\n" + + " \"row\": 0,\n" + + " \"col\": 13\n" + + " },\n" + + " \"6dc68681-97fe-dca5-5df6-b5006797c9eb\": {\n" + + " \"sizeX\": 8,\n" + + " \"sizeY\": 6,\n" + + " \"row\": 6,\n" + + " \"col\": 13\n" + + " }\n" + + " },\n" + + " \"gridSettings\": {\n" + + " \"backgroundColor\": \"#eeeeee\",\n" + + " \"columns\": 24,\n" + + " \"margin\": 10,\n" + + " \"outerMargin\": true,\n" + + " \"backgroundSizeMode\": \"auto\",\n" + + " \"autoFillHeight\": false,\n" + + " \"backgroundImageUrl\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyIAAAKqCAYAAADG2epfAAAABHNCSVQICAgIfAhkiAAAIABJREFUeJzscvuw4MRvxaz5gVL8owhtF0n7gs8zcmnLxWDL3sejVwbSPaER4uzt4fl4saeVj6pOIiIjcIP4fn4t1aCx7Vb0AAAAASUVORK5CYII=\",\n" + + " \"mobileAutoFillHeight\": false,\n" + + " \"mobileRowHeight\": 70\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"entityAliases\": {\n" + + " \"50e40fe7-fd33-e93e-88f6-212acfa1b311\": {\n" + + " \"id\": \"50e40fe7-fd33-e93e-88f6-212acfa1b311\",\n" + + " \"alias\": \"aa\",\n" + + " \"filter\": {\n" + + " \"type\": \"entityList\",\n" + + " \"resolveMultiple\": true,\n" + + " \"entityType\": \"DEVICE\",\n" + + " \"entityList\": [\n" + + " \"5a6c89e0-5ea9-11ed-8ee5-3570b5c0f66a\"\n" + + " ]\n" + + " }\n" + + " }\n" + + " },\n" + + " \"filters\": {},\n" + + " \"timewindow\": {\n" + + " \"hideInterval\": false,\n" + + " \"hideLastInterval\": false,\n" + + " \"hideQuickInterval\": false,\n" + + " \"hideAggregation\": false,\n" + + " \"hideAggInterval\": false,\n" + + " \"hideTimezone\": false,\n" + + " \"selectedTab\": 1,\n" + + " \"history\": {\n" + + " \"historyType\": 1,\n" + + " \"fixedTimewindow\": {\n" + + " \"startTimeMs\": 1697576400000,\n" + + " \"endTimeMs\": 1698181199999\n" + + " },\n" + + " \"interval\": 1210000\n" + + " },\n" + + " \"aggregation\": {\n" + + " \"type\": \"AVG\",\n" + + " \"limit\": 25000\n" + + " }\n" + + " },\n" + + " \"settings\": {\n" + + " \"stateControllerId\": \"entity\",\n" + + " \"showTitle\": false,\n" + + " \"showDashboardsSelect\": true,\n" + + " \"showEntitiesSelect\": true,\n" + + " \"showDashboardTimewindow\": true,\n" + + " \"showDashboardExport\": true,\n" + + " \"toolbarAlwaysOpen\": true,\n" + + " \"titleColor\": \"rgba(0,0,0,0.870588)\",\n" + + " \"showDashboardLogo\": true,\n" + + " \"dashboardLogoUrl\": \"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wCEAAQEBAQEBAUFBQUHBwYHBwoJCAgJCg8KCwoLCg8WDhAODhAOFhQYExITGBQjHBgYHCMpIiAiKTEsLDE+Oz5RlQ8zbG7KF+Bf/Z\",\n" + + " \"hideToolbar\": false,\n" + + " \"showFilters\": true,\n" + + " \"showUpdateDashboardImage\": true,\n" + + " \"dashboardCss\": \"\"\n" + + " }\n" + + "}"; + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 434088180c..a3285a8ecf 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -74,4 +74,6 @@ public interface DashboardService extends EntityDaoService { List findTenantDashboardsByTitle(TenantId tenantId, String title); + PageData findAllDashboardsIds(PageLink pageLink); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index 40125087c2..33fece3292 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -60,4 +60,6 @@ public interface ResourceService extends EntityDaoService { long sumDataSizeByTenantId(TenantId tenantId); + List findByTenantIdAndDataAndKeyStartingWith(TenantId tenantId, String base64Data, String query); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/MediaTypeUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/MediaTypeUtils.java new file mode 100644 index 0000000000..2858293499 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/MediaTypeUtils.java @@ -0,0 +1,22 @@ +package org.thingsboard.server.common.data.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.util.MimeTypeUtils; + +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MediaTypeUtils { + + private static final Map mappings = Map.of( + "jpeg", "jpg", + "svg+xml", "svg" + ); + + public static String getFileExtension(String mimeType) { + String subtype = MimeTypeUtils.parseMimeType(mimeType).getSubtype(); + return mappings.getOrDefault(subtype, subtype); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java index b23bc516bb..54b767446c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java @@ -44,4 +44,6 @@ public interface DashboardDao extends Dao, TenantEntityDao, Exportabl PageData findIdsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAllIds(PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index be1739bc7a..313df7255a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -364,6 +364,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardDao.findByTenantIdAndTitle(tenantId.getId(), title); } + @Override + public PageData findAllDashboardsIds(PageLink pageLink) { + return dashboardDao.findAllIds(pageLink); + } + private final PaginatedRemover tenantDashboardsRemover = new PaginatedRemover<>() { @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 26829bb1af..c896376bc4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -44,6 +44,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; +import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -220,6 +221,12 @@ public class BaseResourceService extends AbstractCachedEntityService findByTenantIdAndDataAndKeyStartingWith(TenantId tenantId, String base64Data, String query) { + String etag = calculateEtag(Base64.getDecoder().decode(base64Data)); + return resourceInfoDao.findByTenantIdAndEtagAndKeyStartingWith(tenantId, etag, query); + } + private String calculateEtag(byte[] data) { return Hashing.sha256().hashBytes(data).toString(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 2354217f1c..c148e70275 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import java.util.List; + public interface TbResourceInfoDao extends Dao { PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink); @@ -31,4 +33,6 @@ public interface TbResourceInfoDao extends Dao { TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); + List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index 8cbe5006b4..4d5027492d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -64,7 +64,8 @@ public class ResourceDataValidator extends DataValidator { @Override protected TbResource validateUpdate(TenantId tenantId, TbResource resource) { - if (resource.getData() != null && !resource.getResourceType().isUpdatable()) { + if (resource.getData() != null && !resource.getResourceType().isUpdatable() && + tenantId != null && !tenantId.isSysTenantId()) { throw new DataValidationException("This type of resource can't be updated"); } return resource; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java index ee6b59e30b..7335d6e072 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java @@ -43,4 +43,7 @@ public interface DashboardRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT id FROM DashboardEntity") + Page findAllIds(Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index 9353a4dbde..2a85692459 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -85,6 +85,11 @@ public class JpaDashboardDao extends JpaAbstractDao return DaoUtil.pageToPageData(dashboardRepository.findIdsByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)).map(DashboardId::new)); } + @Override + public PageData findAllIds(PageLink pageLink) { + return DaoUtil.pageToPageData(dashboardRepository.findAllIds(DaoUtil.toPageable(pageLink)).map(DashboardId::new)); + } + @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java index 7eb5ef1567..f35330eecd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java @@ -31,6 +31,7 @@ import org.thingsboard.server.dao.resource.TbResourceInfoDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -80,4 +81,9 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query) { + return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndHashCodeAndResourceKeyStartingWith(tenantId.getId(), etag, query)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index e01b37e089..7ed0754624 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; +import java.util.List; import java.util.UUID; public interface TbResourceInfoRepository extends JpaRepository { @@ -53,4 +54,6 @@ public interface TbResourceInfoRepository extends JpaRepository findByTenantIdAndHashCodeAndResourceKeyStartingWith(UUID tenantId, String hashCode, String query); + }