diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index cd8521027a..a1e8434b6e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,7 +124,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; -import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -196,7 +195,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - enqueue(tbMsg, MAIN_QUEUE_NAME, onSuccess, onFailure); + enqueue(tbMsg, tbMsg.getQueueName(), onSuccess, onFailure); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 9c72b9a5ad..b23603f6a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -322,12 +323,14 @@ public class TbResourceController extends BaseController { notes = "Deletes the Resource. Referencing non-existing Resource Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/resource/{resourceId}") - public void deleteResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable("resourceId") String strResourceId) throws ThingsboardException { + public ResponseEntity deleteResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable("resourceId") String strResourceId, + @RequestParam(name = "force", required = false) boolean force) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); TbResource tbResource = checkResourceId(resourceId, Operation.DELETE); - tbResourceService.delete(tbResource, getCurrentUser()); + TbResourceDeleteResult tbResourceDeleteResult = tbResourceService.delete(tbResource, force, getCurrentUser()); + return (tbResourceDeleteResult.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(tbResourceDeleteResult); } private ResponseEntity downloadResourceIfChanged(String strResourceId, String etag) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 1c55784f5c..634190c1d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -23,6 +23,7 @@ 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.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -89,7 +90,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements } @Override - public void delete(TbResource tbResource, User user) { + public TbResourceDeleteResult delete(TbResourceInfo tbResource, boolean force, User user) { if (tbResource.getResourceType() == ResourceType.IMAGE) { throw new IllegalArgumentException("Image resource type is not supported"); } @@ -97,8 +98,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceId resourceId = tbResource.getId(); TenantId tenantId = tbResource.getTenantId(); try { - resourceService.deleteResource(tenantId, resourceId); - logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); + TbResourceDeleteResult result = resourceService.deleteResource(tenantId, resourceId, force); + if (result.isSuccess()) { + logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); + } + + return result; } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), actionType, user, e, resourceId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 61a870ccb7..2e6e43e7bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -18,8 +18,9 @@ package org.thingsboard.server.service.resource; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.TbResource; -import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; @@ -37,7 +38,7 @@ public interface TbResourceService { TbResourceInfo save(TbResource entity, SecurityUser user) throws Exception; - void delete(TbResource entity, User user); + TbResourceDeleteResult delete(TbResourceInfo entity, boolean force, User user); List findLwM2mObject(TenantId tenantId, String sortOrder, diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index ea900afd4e..54205327ca 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -131,11 +131,87 @@ class DefaultTbContextTest { defaultTbContext.input(msg, ruleChainId); // THEN + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + + @MethodSource + @ParameterizedTest + public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .queueName(queueName) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + defaultTbContext.enqueue(msg, () -> {}, t -> {}); + + // THEN + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + @MethodSource + @ParameterizedTest + public void givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + defaultTbContext.enqueue(msg, queueName, () -> {}, t -> {}); + + // THEN then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); } private static Stream givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream testQueueNames() { return Stream.of("Main", "Test", null); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index 9688ce8758..320e3815f6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; 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.Device; @@ -60,6 +61,7 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; @@ -79,6 +81,8 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Autowired private DeviceProfileDao deviceProfileDao; + static final String LWM2M_PROFILE_JSON = "{\"name\":\"lwm2m profile\",\"type\":\"DEFAULT\",\"image\":null,\"defaultQueueName\":null,\"transportType\":\"LWM2M\",\"provisionType\":\"DISABLED\",\"description\":\"\",\"profileData\":{\"configuration\":{\"type\":\"DEFAULT\"},\"transportConfiguration\":{\"observeAttr\":{\"observe\":[],\"attribute\":[],\"telemetry\":[\"/11_1.1/0/0\"],\"keyName\":{\"/11_1.1/0/0\":\"profileName\"},\"attributeLwm2m\":{}},\"bootstrap\":[{\"shortServerId\":123,\"bootstrapServerIs\":false,\"host\":\"0.0.0.0\",\"port\":5685,\"clientHoldOffTime\":1,\"serverPublicKey\":\"\",\"serverCertificate\":\"\",\"bootstrapServerAccountTimeout\":0,\"lifetime\":300,\"defaultMinPeriod\":1,\"notifIfDisabled\":true,\"binding\":\"U\",\"securityMode\":\"NO_SEC\"}],\"clientLwM2mSettings\":{\"clientOnlyObserveAfterConnect\":1,\"fwUpdateStrategy\":1,\"swUpdateStrategy\":1,\"powerMode\":\"DRX\",\"edrxCycle\":81000,\"psmActivityTimer\":10000,\"pagingTransmissionWindow\":10000,\"defaultObjectIDVer\":\"1.0\"},\"bootstrapServerUpdateEnable\":false,\"type\":\"LWM2M\"},\"alarms\":null,\"provisionConfiguration\":{\"type\":\"DISABLED\"}}}"; + static class Config { @Bean @Primary @@ -119,7 +123,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertNotNull(savedDeviceProfile.getId()); Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); @@ -135,7 +139,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { ActionType.ADDED); savedDeviceProfile.setName("New device profile"); - doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class); + saveDeviceProfile(savedDeviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); @@ -162,7 +166,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testFindDeviceProfileById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertNotNull(foundDeviceProfile); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); @@ -171,7 +175,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void whenGetDeviceProfileById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + deviceProfile = saveDeviceProfile(deviceProfile); loginDifferentTenant(); @@ -183,7 +187,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testFindDeviceProfileInfoById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/" + savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class); Assert.assertNotNull(foundDeviceProfileInfo); Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); @@ -213,7 +217,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + deviceProfile = saveDeviceProfile(deviceProfile); loginDifferentTenant(); doGet("/api/deviceProfileInfo/" + deviceProfile.getId()) @@ -235,7 +239,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testSetDefaultDeviceProfile() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -328,7 +332,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testChangeDeviceProfileTypeNull() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -345,7 +349,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Device device = new Device(); device.setName("Test device"); device.setType("default"); @@ -367,7 +371,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfileWithExistingDevice() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Device device = new Device(); device.setName("Test device"); @@ -419,7 +423,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public void testSaveDeviceProfileWithFirmwareFromDifferentTenant() throws Exception { loginDifferentTenant(); DeviceProfile differentProfile = createDeviceProfile("Different profile"); - differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class); + differentProfile = saveDeviceProfile(differentProfile); SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); firmwareInfo.setDeviceProfileId(differentProfile.getId()); firmwareInfo.setType(FIRMWARE); @@ -441,7 +445,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public void testSaveDeviceProfileWithSoftwareFromDifferentTenant() throws Exception { loginDifferentTenant(); DeviceProfile differentProfile = createDeviceProfile("Different profile"); - differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class); + differentProfile = saveDeviceProfile(differentProfile); SaveOtaPackageInfoRequest softwareInfo = new SaveOtaPackageInfoRequest(); softwareInfo.setDeviceProfileId(differentProfile.getId()); softwareInfo.setType(SOFTWARE); @@ -462,7 +466,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfile() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -495,7 +499,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { int cntEntity = 28; for (int i = 0; i < cntEntity; i++) { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i); - deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); } testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new DeviceProfile(), new DeviceProfile(), @@ -552,7 +556,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { for (int i = 0; i < 28; i++) { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i); - deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); } List loadedDeviceProfileInfos = new ArrayList<>(); @@ -961,7 +965,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { JsonTransportPayloadConfiguration jsonTransportPayloadConfiguration = new JsonTransportPayloadConfiguration(); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(jsonTransportPayloadConfiguration, true); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT); Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration); @@ -979,7 +983,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { "v1/devices/me/telemetry", "v1/devices/me/attributes", "v1/devices/me/subscribeattributes"); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT); Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration); @@ -997,7 +1001,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = this.createProtoTransportPayloadConfiguration(schema, schema, null, null); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration, false); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); @@ -1036,14 +1040,14 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfileWithDeleteRelationsOk() throws Exception { - DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); + DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); testEntityDaoWithRelationsOk(savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } @Ignore @Test public void testDeleteDeviceProfileExceptionWithRelationsTransactional() throws Exception { - DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); + DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); testEntityDaoWithRelationsTransactionalException(deviceProfileDao, savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } @@ -1103,8 +1107,36 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(count, deviceProfileNames.size()); } - private DeviceProfile savedDeviceProfile(String name) { + @Test + public void testSaveDeviceProfileWithOutdatedVersion() throws Exception { + DeviceProfile deviceProfile = JacksonUtil.fromString(LWM2M_PROFILE_JSON, DeviceProfile.class); + deviceProfile.setName("Device profile v1.0"); + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getVersion()).isOne(); + + deviceProfile.setName("Device profile v2.0"); + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getVersion()).isEqualTo(2); + + deviceProfile.setName("Device profile v1.1"); + deviceProfile.setVersion(1L); + String response = doPost("/api/deviceProfile", deviceProfile).andExpect(status().isConflict()) + .andReturn().getResponse().getContentAsString(); + assertThat(JacksonUtil.toJsonNode(response).get("message").asText()) + .containsIgnoringCase("already changed by someone else"); + + deviceProfile.setVersion(null); // overriding entity + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getName()).isEqualTo("Device profile v1.1"); + assertThat(deviceProfile.getVersion()).isEqualTo(3); + } + + private DeviceProfile saveDeviceProfile(String name) { DeviceProfile deviceProfile = createDeviceProfile(name); + return saveDeviceProfile(deviceProfile); + } + + private DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); } diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index c4074f0d00..0d7b81370a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Assert; @@ -27,7 +28,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; @@ -40,13 +44,16 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mObject; 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.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeInfo; 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.Collections; +import java.util.HashMap; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -63,7 +70,6 @@ public class TbResourceControllerTest extends AbstractControllerTest { private static final String JS_TEST_FILE_NAME = "test.js"; private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; - private Tenant savedTenant; private User tenantAdmin; @@ -222,28 +228,196 @@ public class TbResourceControllerTest extends AbstractControllerTest { } @Test - public void testShoudNotDeleteTbResourceIfAssignedToWidgetType() throws Exception { + public void testUnForcedDeleteTbResourceIfAssignedToWidgetType() throws Exception { TbResource resource = new TbResource(); - resource.setResourceType(ResourceType.JKS); + resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("My first resource"); - resource.setFileName(DEFAULT_FILE_NAME); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); TbResourceInfo savedResource = save(resource); - Mockito.reset(tbClusterService, auditLogService); - String resourceIdStr = savedResource.getId().getId().toString(); + var link = resource.getLink(); + WidgetTypeDetails widgetType = new WidgetTypeDetails(); + widgetType.setName("Widget Type"); + widgetType.setTenantId(savedTenant.getId()); + widgetType.setDescriptor(JacksonUtil.newObjectNode() + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config") + .put("resource", link)); + WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); + + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=false") + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertFalse(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + Assert.assertNotNull(referenceValues); + + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNotNull(widgetTypeInfos); + Assert.assertFalse(widgetTypeInfos.isEmpty()); + Assert.assertEquals(1, widgetTypeInfos.size()); + + var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); + Assert.assertNotNull(dashboardInfo); - //create widget type + WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class); + Assert.assertNotNull(foundedWidgetType); + Assert.assertEquals(foundedWidgetType, dashboardInfo); + } + + @Test + public void testForcedDeleteTbResourceIfAssignedToWidgetType() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My first resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); + TbResourceInfo savedResource = save(resource); + + var link = resource.getLink(); WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":\"tb-resource;/api/resource/jks/tenant/%s\",\"isModule\":true}]}", savedResource.getResourceKey()), JsonNode.class)); + widgetType.setTenantId(savedTenant.getId()); + widgetType.setDescriptor(JacksonUtil.newObjectNode() + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config") + .put("resource", link)); doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); - doDelete("/api/resource/" + resourceIdStr) + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=true") + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertTrue(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNull(widgetTypeInfos); + } + + @Test + public void testUnForcedDeleteTbResourceIfAssignedToDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My first resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); + TbResourceInfo savedResource = save(resource); + + var link = resource.getLink(); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .set("widgets", JacksonUtil.toJsonNode(""" + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """)) + .put("resource", link)); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=false") .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Following widget types use this resource: " - + widgetType.getName()))); + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertFalse(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + Assert.assertNotNull(referenceValues); + + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNotNull(dashboardInfos); + Assert.assertFalse(dashboardInfos.isEmpty()); + Assert.assertEquals(1, dashboardInfos.size()); + + var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); + Assert.assertNotNull(dashboardInfo); + + DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class); + Assert.assertNotNull(foundDashboard); + Assert.assertEquals(foundDashboard, dashboardInfo); + } + + @Test + public void testForcedDeleteTbResourceIfAssignedToDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My first resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); + TbResourceInfo savedResource = save(resource); + + var link = resource.getLink(); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .set("widgets", JacksonUtil.toJsonNode(""" + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """)) + .put("resource", link)); + doPost("/api/dashboard", dashboard, Dashboard.class); + + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=true") + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertTrue(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNull(dashboardInfos); } @Test @@ -676,7 +850,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() {}); + doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() { + }); Assert.assertNotNull(objects); Assert.assertEquals(resources.size(), objects.size()); @@ -690,7 +865,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.2,5_1.2,19_1.1", new TypeReference<>() {}); + doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.2,5_1.2,19_1.1", new TypeReference<>() { + }); Assert.assertNotNull(objects); Assert.assertEquals(3, objects.size()); diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index 2ec917c81a..fe416eacd5 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -16,15 +16,22 @@ package org.thingsboard.server.service.resource.sql; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; @@ -36,10 +43,14 @@ 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.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.service.resource.TbResourceService; import java.util.ArrayList; @@ -108,6 +119,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { ""; private static final String DEFAULT_FILE_NAME = "test.jks"; + private static final String JS_FILE_NAME = "test.js"; private static final String TEST_BASE64_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; private static final byte[] TEST_DATA = Base64.getDecoder().decode(TEST_BASE64_DATA); @@ -119,6 +131,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { private ResourceService resourceService; @Autowired private TbResourceService tbResourceService; + @Autowired + private WidgetTypeService widgetTypeService; + @Autowired + private DashboardService dashboardService; private Tenant savedTenant; private User tenantAdmin; @@ -141,6 +157,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { tenantAdmin.setLastName("Downs"); tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } @After @@ -244,7 +261,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(title, foundResource.getTitle()); assertArrayEquals(foundResource.getData(), TEST_DATA); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(foundResource, true, null); } @Test @@ -267,7 +284,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals("0_1.0", foundResource.getResourceKey()); assertArrayEquals(foundResource.getData(), LWM2M_TEST_MODEL.getBytes()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(savedResource, true, null); } @Test @@ -281,8 +298,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); - TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(savedResource, true, null); } @Test @@ -361,7 +377,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(foundResource); assertEquals(savedResource, new TbResourceInfo(foundResource)); assertArrayEquals(TEST_DATA, foundResource.getData()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(foundResource, true, null); } @Test @@ -378,22 +394,207 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(foundResource); assertEquals(savedResource, new TbResourceInfo(foundResource)); assertArrayEquals(TEST_DATA, foundResource.getData()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(foundResource, true, null); } @Test public void testDeleteResource() throws Exception { TbResource resource = new TbResource(); - resource.setResourceType(ResourceType.JKS); + resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); TbResourceInfo savedResource = tbResourceService.save(resource); - TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + tbResourceService.delete(savedResource, true, null); + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNull(foundResource); + } + + @Test + public void testUnForceDeleteResourceAssignWithWidget() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(); + widgetTypeDetails.setTenantId(savedTenant.getId()); + widgetTypeDetails.setDescriptor(JacksonUtil.newObjectNode() + .put("sizeX", 3) + .put("sizeY", 3) + .put("resource", link) + .put("templateCss", "") + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config")); + widgetTypeDetails.setName("Widget Type"); + + WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails); + WidgetTypeDetails foundWidgetType = widgetTypeService.findWidgetTypeDetailsById(savedTenant.getId(), savedWidgetType.getId()); + String resourceLink = foundWidgetType.getDescriptor().get("resource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + Assert.assertNotNull(result); + Assert.assertFalse(result.isSuccess()); + Assert.assertFalse(result.getReferences().isEmpty()); + Assert.assertEquals(1, result.getReferences().size()); + + WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); + WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType); + Assert.assertNotNull(widgetTypeInfo); + Assert.assertNotNull(foundWidgetTypeInfo); + Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo); + + TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedResource, foundResourceInfo); + } + + @Test + public void testForceDeleteResourceAssignWithWidget() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(); + widgetTypeDetails.setTenantId(savedTenant.getId()); + widgetTypeDetails.setDescriptor(JacksonUtil.newObjectNode() + .put("type", "rpc") + .put("sizeX", 3) + .put("sizeY", 3) + .put("resource", link) + .put("templateCss", "") + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config")); + widgetTypeDetails.setName("Widget Type"); + + WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails); + WidgetTypeDetails foundWidgetType = widgetTypeService.findWidgetTypeDetailsById(savedTenant.getId(), savedWidgetType.getId()); + String resourceLink = foundWidgetType.getDescriptor().get("resource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, true, null); + Assert.assertNotNull(result); + Assert.assertTrue(result.isSuccess()); + Assert.assertNull(result.getReferences()); + + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNull(foundResource); + } + + @Test + public void testUnForceDeleteResourceAssignWithDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .put("widgets", """ + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """) + .put("someResource", link)); + + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); + Dashboard foundDashboard = dashboardService.findDashboardById(savedTenant.getId(), savedDashboard.getId()); + String resourceLink = foundDashboard.getConfiguration().get("someResource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + Assert.assertNotNull(result); + Assert.assertFalse(result.isSuccess()); + Assert.assertNotNull(result.getReferences()); + Assert.assertEquals(1, result.getReferences().size()); + + DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); + DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId()); + Assert.assertNotNull(dashboardInfo); + Assert.assertNotNull(foundDashboardInfo); + Assert.assertEquals(foundDashboardInfo, dashboardInfo); + + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedDashboard, foundDashboard); + } + + @Test + public void testForceDeleteResourceAssignWithDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); - tbResourceService.delete(foundResource, null); - foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .set("widgets", JacksonUtil.toJsonNode(""" + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """)) + .put("someResource", link)); + + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); + Dashboard foundDashboard = dashboardService.findDashboardById(savedTenant.getId(), savedDashboard.getId()); + String resourceLink = foundDashboard.getConfiguration().get("someResource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, true, null); + Assert.assertNotNull(result); + Assert.assertTrue(result.isSuccess()); + Assert.assertNull(result.getReferences()); + + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNull(foundResource); } 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 d6074ca48b..ee187db46b 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 @@ -21,6 +21,7 @@ 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.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.TbResourceId; @@ -69,9 +70,7 @@ public interface ResourceService extends EntityDaoService { PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType lwm2mModel, PageLink pageLink); - void deleteResource(TenantId tenantId, TbResourceId resourceId); - - void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force); + TbResourceDeleteResult deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force); void deleteResourcesByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java new file mode 100644 index 0000000000..edc5a2f539 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 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; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.HasId; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class TbResourceDeleteResult { + + private boolean success; + private Map>> references; + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java index 4528819e44..c674407a00 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java @@ -37,6 +37,7 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.util.Locale; import java.util.function.BiFunction; +import java.util.function.Function; public class TbDate implements Serializable, Cloneable { @@ -481,6 +482,43 @@ public class TbDate implements Serializable, Cloneable { instant = Instant.ofEpochMilli(dateMilliSecond); } + public void addDays(int days) { + adjustTime(zonedDateTime -> zonedDateTime.plusDays(days)); + } + + public void addYears(int years) { + adjustTime(zonedDateTime -> zonedDateTime.plusYears(years)); + } + + public void addMonths(int months) { + adjustTime(zonedDateTime -> zonedDateTime.plusMonths(months)); + } + + public void addWeeks(int weeks) { + adjustTime(zonedDateTime -> zonedDateTime.plusWeeks(weeks)); + } + + public void addHours(int hours) { + adjustTime(zonedDateTime -> zonedDateTime.plusHours(hours)); + } + + public void addMinutes(int minutes) { + adjustTime(zonedDateTime -> zonedDateTime.plusMinutes(minutes)); + } + + public void addSeconds(int seconds) { + adjustTime(zonedDateTime -> zonedDateTime.plusSeconds(seconds)); + } + + public void addNanos(long nanos) { + adjustTime(zonedDateTime -> zonedDateTime.plusNanos(nanos)); + } + + private void adjustTime(Function adjuster) { + ZonedDateTime zonedDateTime = adjuster.apply(getZonedDateTime()); + this.instant = zonedDateTime.toInstant(); + } + public ZoneOffset getLocaleZoneOffset(Instant... instants){ return ZoneId.systemDefault().getRules().getOffset(instants.length > 0 ? instants[0] : this.instant); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index 9492c2e610..e001e9fb11 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -885,4 +885,64 @@ class TbDateTest { Assertions.assertNotNull(serializedTbDate); Assertions.assertEquals(expectedDate.toString(), serializedTbDate); } + + @Test + void testAddFunctions() { + TbDate d = new TbDate(2024, 1, 1, 10, 0, 0, 0); + testResultChangeDateTime(d); + + d.addYears(1); + testResultChangeDateTime(d); + + d.addYears(-2); + testResultChangeDateTime(d); + + d.addMonths(2); + testResultChangeDateTime(d); + + d.addMonths(10); + testResultChangeDateTime(d); + + d.addMonths(-13); + testResultChangeDateTime(d); + + d.addWeeks(4); + testResultChangeDateTime(d); + + d.addWeeks(-5); + testResultChangeDateTime(d); + + d.addDays(6); + testResultChangeDateTime(d); + + d.addDays(45); + testResultChangeDateTime(d); + + d.addDays(-50); + testResultChangeDateTime(d); + + d.addHours(23); + testResultChangeDateTime(d); + + d.addHours(-47); + testResultChangeDateTime(d); + + d.addMinutes(59); + testResultChangeDateTime(d); + + d.addMinutes(-60); + testResultChangeDateTime(d); + + d.addSeconds(59); + testResultChangeDateTime(d); + + d.addSeconds(-60); + testResultChangeDateTime(d); + + d.addNanos(999999); + testResultChangeDateTime(d); + + d.addNanos(-1000000); + testResultChangeDateTime(d); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java new file mode 100644 index 0000000000..6a952fd501 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 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.dao; + +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +public interface ResourceContainerDao> { + + List findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit); + + List findByResourceLink(String link, int limit); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 088b9e8a1f..6a3ec3295e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ImageContainerDao; +import org.thingsboard.server.dao.ResourceContainerDao; import java.util.UUID; /** * The Interface DashboardInfoDao. */ -public interface DashboardInfoDao extends Dao, ImageContainerDao { +public interface DashboardInfoDao extends Dao, ImageContainerDao, ResourceContainerDao { /** * Find dashboards by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index f8fe55cf02..e91c1939f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -75,8 +75,6 @@ import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; @Slf4j public class BaseImageService extends BaseResourceService implements ImageService { - private static final int MAX_ENTITIES_TO_FIND = 10; - public static Map DASHBOARD_BASE64_MAPPING = new HashMap<>(); public static Map WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); @@ -107,19 +105,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic private final AssetProfileDao assetProfileDao; private final DeviceProfileDao deviceProfileDao; private final WidgetsBundleDao widgetsBundleDao; - private final WidgetTypeDao widgetTypeDao; - private final DashboardInfoDao dashboardInfoDao; private final Map> imageContainerDaoMap = new HashMap<>(); public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { - super(resourceDao, resourceInfoDao, resourceValidator); + super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao); this.assetProfileDao = assetProfileDao; this.deviceProfileDao = deviceProfileDao; this.widgetsBundleDao = widgetsBundleDao; - this.widgetTypeDao = widgetTypeDao; - this.dashboardInfoDao = dashboardInfoDao; } @PostConstruct @@ -131,7 +125,6 @@ public class BaseImageService extends BaseResourceService implements ImageServic imageContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); } - @Override @SneakyThrows public TbResourceInfo saveImage(TbResource image) { @@ -311,7 +304,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic } } if (success) { - deleteResource(tenantId, imageId, force); + success = deleteResource(tenantId, imageId, true) + .isSuccess(); } return result.success(success).build(); } 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 76aefdbb93..bf73c59708 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 @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -39,6 +40,7 @@ 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.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.EntityId; @@ -48,6 +50,8 @@ import org.thingsboard.server.common.data.id.TenantId; 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 org.thingsboard.server.dao.ResourceContainerDao; +import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -55,6 +59,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; +import org.thingsboard.server.dao.widget.WidgetTypeDao; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -85,6 +90,17 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceContainerDaoMap = new HashMap<>(); + protected static final int MAX_ENTITIES_TO_FIND = 10; + + @PostConstruct + public void init() { + resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); + resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + } + @Autowired @Lazy private ImageService imageService; @@ -313,23 +329,45 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); TbResourceInfo resource = findResourceInfoById(tenantId, resourceId); + boolean success = true; + var result = TbResourceDeleteResult.builder(); + if (resource == null) { - return; + if (!force) { + success = false; + } + return result.success(success).build(); } + if (!force) { - resourceValidator.validateDelete(tenantId, resource); + if (resource.getResourceType() == ResourceType.JS_MODULE) { + var link = resource.getLink(); + Map>> affectedEntities = new HashMap<>(); + + resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> { + var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) : + resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND); + if (!entities.isEmpty()) { + affectedEntities.put(entityType.name(), entities); + } + }); + + if (!affectedEntities.isEmpty()) { + success = false; + result.references(affectedEntities); + } + } } - resourceDao.removeById(tenantId, resourceId.getId()); - eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); + if (success) { + resourceDao.removeById(tenantId, resourceId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); + } + + return result.success(success).build(); } @Override @@ -666,7 +704,7 @@ public class BaseResourceService extends AbstractCachedEntityService { @Autowired private TbResourceDao resourceDao; - @Autowired - private WidgetTypeDao widgetTypeDao; - @Autowired private TenantService tenantService; @@ -111,12 +104,4 @@ public class ResourceDataValidator extends DataValidator { validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE); } } - - public void validateDelete(TenantId tenantId, TbResourceInfo resourceInfo) { - List widgets = widgetTypeDao.findWidgetTypesNamesByTenantIdAndResourceLink(tenantId.getId(), resourceInfo.getLink()); - if (!widgets.isEmpty()) { - throw new DataValidationException("Following widget types use this resource: " + String.join(", ", widgets)); - } - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 0f2550b7a2..05577b68e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -88,10 +88,10 @@ public abstract class JpaAbstractDao, D> boolean flushed = false; EntityManager entityManager = getEntityManager(); if (isNew) { + entityManager.persist(entity); if (entity instanceof HasVersion versionedEntity) { versionedEntity.setVersion(1L); } - entityManager.persist(entity); } else { if (entity instanceof HasVersion versionedEntity) { if (versionedEntity.getVersion() == null) { @@ -106,23 +106,25 @@ public abstract class JpaAbstractDao, D> } } versionedEntity = entityManager.merge(versionedEntity); + entity = (E) versionedEntity; /* * by default, Hibernate doesn't issue an update query and thus version increment * if the entity was not modified. to bypass this and always increment the version, we do it manually * */ versionedEntity.setVersion(versionedEntity.getVersion() + 1); - /* - * flushing and then removing the entity from the persistence context so that it is not affected - * by next flushes (e.g. when a transaction is committed) to avoid double version increment - * */ - entityManager.flush(); - entityManager.detach(versionedEntity); - flushed = true; - entity = (E) versionedEntity; } else { entity = entityManager.merge(entity); } } + if (entity instanceof HasVersion versionedEntity) { + /* + * flushing and then removing the entity from the persistence context so that it is not affected + * by next flushes (e.g. when a transaction is committed) to avoid double version increment + * */ + entityManager.flush(); + entityManager.detach(versionedEntity); + flushed = true; + } if (flush && !flushed) { entityManager.flush(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index cddbe3e347..7624ddc738 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -78,13 +78,21 @@ public interface DashboardInfoRepository extends JpaRepository findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("limit") int limit); @Query(nativeQuery = true, - value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt" + value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :limit" ) - List findByImageLink(@Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit); + + @Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", + nativeQuery = true) + List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + + @Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", + nativeQuery = true) + List findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 249e9d6e5b..bc07139725 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -133,4 +133,15 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByImageLink(String imageLink, int limit) { return DaoUtil.convertDataList(dashboardInfoRepository.findByImageLink(imageLink, limit)); } + + @Override + public List findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) { + return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit)); + } + + @Override + public List findByResourceLink(String link, int limit) { + return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index 5d207800d7..df10804d50 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -190,11 +190,6 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findWidgetTypesNamesByTenantIdAndResourceLink(UUID tenantId, String link) { - return widgetTypeRepository.findNamesByTenantIdAndResourceLink(tenantId, link); - } - @Override public List findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List widgetFqns) { var idFqnPairs = widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns); @@ -266,4 +261,14 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) { + return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit)); + } + + @Override + public List findByResourceLink(String link, int limit) { + return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index 39ba949ca9..dc79280bcf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -204,13 +204,20 @@ public interface WidgetTypeInfoRepository extends JpaRepository findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("limit") int limit); @Query(nativeQuery = true, value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " + - "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :lmt)" + "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :limit)" ) - List findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit); + + @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) + List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + + @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) + List findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index d92bd085cf..e148c10d74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java @@ -69,12 +69,6 @@ public interface WidgetTypeRepository extends JpaRepository> 'resources' LIKE concat('%', :resourceLink, '%')", - nativeQuery = true) - List findNamesByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, - @Param("resourceLink") String resourceLink); - @Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java index 7302ed41bc..bbb8d026c0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; +import org.thingsboard.server.dao.ResourceContainerDao; import java.util.List; import java.util.UUID; @@ -35,7 +36,7 @@ import java.util.UUID; /** * The Interface WidgetTypeDao. */ -public interface WidgetTypeDao extends Dao, ExportableEntityDao, ImageContainerDao { +public interface WidgetTypeDao extends Dao, ExportableEntityDao, ImageContainerDao, ResourceContainerDao { /** * Save or update widget type object @@ -97,8 +98,6 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn); - List findWidgetTypesNamesByTenantIdAndResourceLink(UUID tenantId, String link); - List findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List widgetFqns); List findWidgetsBundleWidgetsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d743867b84..46b63e0595 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -143,8 +143,7 @@ public interface TbContext { void tellFailure(TbMsg msg, Throwable th); /** - * Puts new message to queue for processing by the Root Rule Chain - * WARNING: message is put to the Main queue. To specify other queue name - use {@link #enqueue(TbMsg, String, Runnable, Consumer)} + * Puts new message to queue from TbMsg for processing by the Root Rule Chain * * @param msg - message */ diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 9f0daa73ee..615b721b97 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -90,8 +90,8 @@ export class ResourceService { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } - public deleteResource(resourceId: string, config?: RequestConfig) { - return this.http.delete(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); + public deleteResource(resourceId: string, force = false, config?: RequestConfig) { + return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html index 3aa743247f..1132c9982c 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/kafka-config.component.html @@ -73,20 +73,6 @@ - - rule-node-config.key-serializer - - - {{ 'rule-node-config.key-serializer-required' | translate }} - - - - rule-node-config.value-serializer - - - {{ 'rule-node-config.value-serializer-required' | translate }} - - , private resourceService: ResourceService, private translate: TranslateService, + private dialog: MatDialog, + private dialogService: DialogService, private router: Router, private datePipe: DatePipe) { @@ -81,20 +97,16 @@ export class JsLibraryTableConfigResolver { entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), ); - this.config.cellActionDescriptors.push( - { - name: this.translate.instant('javascript.download'), - icon: 'file_download', - isEnabled: () => true, - onAction: ($event, entity) => this.downloadResource($event, entity) - } - ); + this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store)); - this.config.deleteEntityTitle = resource => this.translate.instant('javascript.delete-javascript-resource-title', - { resourceTitle: resource.title }); - this.config.deleteEntityContent = () => this.translate.instant('javascript.delete-javascript-resource-text'); - this.config.deleteEntitiesTitle = count => this.translate.instant('javascript.delete-javascript-resources-title', {count}); - this.config.deleteEntitiesContent = () => this.translate.instant('javascript.delete-javascript-resources-text'); + this.config.groupActionDescriptors = [{ + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: true, + onAction: ($event, entities) => this.deleteResources($event, entities) + }]; + + this.config.entitiesDeleteEnabled = false; this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, ResourceType.JS_MODULE, this.config.componentsData.resourceSubType); this.config.loadEntity = id => { @@ -115,7 +127,6 @@ export class JsLibraryTableConfigResolver { } return saveObservable; }; - this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); this.config.onEntityAction = action => this.onResourceAction(action); } @@ -126,7 +137,6 @@ export class JsLibraryTableConfigResolver { resourceSubType: '' }; const authUser = getCurrentAuthUser(this.store); - this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority); return this.config; @@ -170,4 +180,151 @@ export class JsLibraryTableConfigResolver { return authority === Authority.SYS_ADMIN; } } + + private deleteResource($event: Event, resource: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('javascript.delete-javascript-resource-title', { resourceTitle: resource.title }), + this.translate.instant('javascript.delete-javascript-resource-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ).subscribe( + (deleteResult) => { + if (deleteResult.success) { + this.config.updateData(); + } else if (deleteResult.resourceIsReferencedError) { + const resources: ResourceInfoWithReferences[] = [{...resource, ...{references: deleteResult.references}}]; + const data = { + multiple: false, + resources, + configuration: { + title: 'javascript.javascript-resource-is-in-use', + message: this.translate.instant('javascript.javascript-resource-is-in-use-text', {title: resources[0].title}), + deleteText: 'javascript.delete-javascript-resource-in-use-text', + selectedText: 'javascript.selected-javascript-resources', + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((resources) => { + if (resources) { + this.resourceService.deleteResource(resource.id.id, true).subscribe( + () => { + this.config.updateData(); + } + ); + } + }); + } else { + const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + + private deleteResources($event: Event, resources: ResourceInfo[]) { + if ($event) { + $event.stopPropagation(); + } + if (resources && resources.length) { + const title = this.translate.instant('javascript.delete-javascript-resources-title', {count: resources.length}); + const content = this.translate.instant('javascript.delete-javascript-resources-text'); + this.dialogService.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe((result) => { + if (result) { + const tasks = resources.map((resource) => + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ) + ); + forkJoin(tasks).subscribe( + (deleteResults) => { + const anySuccess = deleteResults.some(res => res.success); + const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError); + const otherError = deleteResults.find(res => !res.success); + if (anySuccess) { + this.config.updateData(); + } + if (referenceErrors?.length) { + const resourcesWithReferences: ResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}})); + const data = { + multiple: true, + resources: resourcesWithReferences, + configuration: { + title: 'javascript.javascript-resources-are-in-use', + message: this.translate.instant('javascript.javascript-resources-are-in-use-text'), + deleteText: 'javascript.delete-javascript-resource-in-use-text', + selectedText: 'javascript.selected-javascript-resources', + datasource: new ResourcesDatasource(this.resourceService, resourcesWithReferences, entity => true), + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((forceDeleteResources) => { + if (forceDeleteResources && forceDeleteResources.length) { + const forceDeleteTasks = forceDeleteResources.map((resource) => + this.resourceService.deleteResource(resource.id.id, true) + ); + forkJoin(forceDeleteTasks).subscribe( + () => { + this.config.updateData(); + } + ); + } + }); + } else if (otherError) { + const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + } + + private configureCellActions(authUser: AuthUser): Array> { + const actions: Array> = []; + actions.push( + { + name: this.translate.instant('javascript.download'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.downloadResource($event, entity) + }, + { + name: this.translate.instant('javascript.delete'), + icon: 'delete', + isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority), + onAction: ($event, entity) => this.deleteResource($event, entity) + }, + ); + return actions; + } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts new file mode 100644 index 0000000000..ed9c9f5dd9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts @@ -0,0 +1,130 @@ +/// +/// Copyright © 2016-2025 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. +/// + +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { ResourceInfo, ResourceSubType, ResourceType } from '@shared/models/resource.models'; +import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, take, tap } from 'rxjs/operators'; +import { ResourceService } from "@core/http/resource.service"; + +export class ResourcesDatasource implements DataSource { + private entitiesSubject: Subject; + private readonly pageDataSubject: Subject>; + + public pageData$: Observable>; + + public selection = new SelectionModel(true, []); + + public dataLoading = true; + + constructor(private resourceService: ResourceService, + private resources: ResourceInfo[], + private selectionEnabledFunction: EntityBooleanFunction) { + if (this.resources && this.resources.length) { + this.entitiesSubject = new BehaviorSubject(this.resources); + } else { + this.entitiesSubject = new BehaviorSubject([]); + this.pageDataSubject = new BehaviorSubject>(emptyPageData()); + this.pageData$ = this.pageDataSubject.asObservable(); + } + } + + connect(collectionViewer: CollectionViewer): + Observable> { + return this.entitiesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.entitiesSubject.complete(); + if (this.pageDataSubject) { + this.pageDataSubject.complete(); + } + } + + reset() { + this.entitiesSubject.next([]); + if (this.pageDataSubject) { + this.pageDataSubject.next(emptyPageData()); + } + } + + loadEntities(pageLink: PageLink, resourceType: ResourceType, subType: ResourceSubType): Observable> { + this.dataLoading = true; + const result = new ReplaySubject>(); + this.fetchEntities(pageLink, resourceType, subType).pipe( + tap(() => { + this.selection.clear(); + }), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.entitiesSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoading = false; + } + ); + return result; + } + + fetchEntities(pageLink: PageLink, resourceType: ResourceType, subType: ResourceSubType): Observable> { + return this.resourceService.getResources(pageLink, resourceType, subType); + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.entitiesSubject.pipe( + map((entities) => numSelected === entities.length) + ); + } + + isEmpty(): Observable { + return this.entitiesSubject.pipe( + map((entities) => !entities.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + masterToggle() { + this.entitiesSubject.pipe( + tap((entities) => { + const numSelected = this.selection.selected.length; + if (numSelected === this.selectableEntitiesCount(entities)) { + this.selection.clear(); + } else { + entities.forEach(row => { + if (this.selectionEnabledFunction(row)) { + this.selection.select(row); + } + }); + } + }), + take(1) + ).subscribe(); + } + + private selectableEntitiesCount(entities: Array): number { + return entities.filter((entity) => this.selectionEnabledFunction(entity)).length; + } +} diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts index ef22c037f2..6e40683c2b 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -16,10 +16,10 @@ import { ImageResourceInfo, - ImageResourceInfoWithReferences, + ResourceInfoWithReferences, imageResourceType, ResourceSubType, - toImageDeleteResult + toResourceDeleteResult } from '@shared/models/resource.models'; import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { ImageService } from '@core/http/image.service'; @@ -67,9 +67,9 @@ import { ImageDialogComponent, ImageDialogData } from '@shared/components/image/ import { ImportExportService } from '@shared/import-export/import-export.service'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { - ImagesInUseDialogComponent, - ImagesInUseDialogData -} from '@shared/components/image/images-in-use-dialog.component'; + ResourcesInUseDialogComponent, + ResourcesInUseDialogData +} from '@shared/components/resource/resources-in-use-dialog.component'; import { ImagesDatasource } from '@shared/components/image/images-datasource'; import { EmbedImageDialogComponent, EmbedImageDialogData } from '@shared/components/image/embed-image-dialog.component'; @@ -504,21 +504,30 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe this.translate.instant('action.yes')).subscribe((result) => { if (result) { this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe( - map(() => toImageDeleteResult(image)), - catchError((err) => of(toImageDeleteResult(image, err))) + map(() => toResourceDeleteResult(image)), + catchError((err) => of(toResourceDeleteResult(image, err))) ).subscribe( (deleteResult) => { if (deleteResult.success) { this.imageDeleted(itemIndex); - } else if (deleteResult.imageIsReferencedError) { - this.dialog.open(ImagesInUseDialogComponent, { + } else if (deleteResult.resourceIsReferencedError) { + const images = [{...image, ...{references: deleteResult.references}}]; + const data = { + multiple: false, + resources: images, + configuration: { + title: 'image.image-is-in-use', + message: this.translate.instant('image.image-is-in-use-text', {title: images[0].title}), + deleteText: 'image.delete-image-in-use-text', + selectedText: 'image.selected-images', + columns: ['select', 'preview', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - multiple: false, - images: [{...image, ...{references: deleteResult.references}}] - } + data }).afterClosed().subscribe((images) => { if (images) { this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true).subscribe( @@ -554,29 +563,38 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe if (result) { const tasks = selectedImages.map((image) => this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe( - map(() => toImageDeleteResult(image)), - catchError((err) => of(toImageDeleteResult(image, err))) + map(() => toResourceDeleteResult(image)), + catchError((err) => of(toResourceDeleteResult(image, err))) ) ); forkJoin(tasks).subscribe( (deleteResults) => { const anySuccess = deleteResults.some(res => res.success); - const referenceErrors = deleteResults.filter(res => res.imageIsReferencedError); + const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError); const otherError = deleteResults.find(res => !res.success); if (anySuccess) { this.updateData(); } if (referenceErrors?.length) { - const imagesWithReferences: ImageResourceInfoWithReferences[] = - referenceErrors.map(ref => ({...ref.image, ...{references: ref.references}})); - this.dialog.open(ImagesInUseDialogComponent, { + const imagesWithReferences: ResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}})); + const data = { + multiple: true, + resources: imagesWithReferences, + configuration: { + title: 'image.images-are-in-use', + message: this.translate.instant('image.images-are-in-use-text'), + deleteText: 'image.delete-image-in-use-text', + selectedText: 'image.selected-images', + columns: ['select', 'preview', 'title', 'references'], + datasource: new ImagesDatasource(null, imagesWithReferences, entity => true) + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - multiple: true, - images: imagesWithReferences - } + data }).afterClosed().subscribe((forceDeleteImages) => { if (forceDeleteImages && forceDeleteImages.length) { const forceDeleteTasks = forceDeleteImages.map((image) => diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.ts b/ui-ngx/src/app/shared/components/image/image-references.component.ts index af2b4e9798..802408ee3a 100644 --- a/ui-ngx/src/app/shared/components/image/image-references.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-references.component.ts @@ -17,7 +17,7 @@ import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { ImageReferences } from '@shared/models/resource.models'; +import { ResourceReferences } from '@shared/models/resource.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { getEntityDetailsPageURL } from '@core/utils'; @@ -54,7 +54,7 @@ type ReferencedEntitiesEntry = [string, TenantReferencedEntities]; export class ImageReferencesComponent implements OnInit { @Input() - references: ImageReferences; + references: ResourceReferences; popoverComponent: TbPopoverComponent; @@ -99,7 +99,7 @@ export class ImageReferencesComponent implements OnInit { return tenantId === NULL_UUID; } - private hasNonSystemEntities(references: ImageReferences): boolean { + private hasNonSystemEntities(references: ResourceReferences): boolean { for (const entityTypeStr of Object.keys(references)) { const entities = this.references[entityTypeStr]; if (entities.some(e => e.tenantId && e.tenantId.id && e.tenantId.id !== NULL_UUID)) { @@ -109,7 +109,7 @@ export class ImageReferencesComponent implements OnInit { return false; } - private toReferencedEntitiesList(references: ImageReferences): ReferencedEntityInfo[] { + private toReferencedEntitiesList(references: ResourceReferences): ReferencedEntityInfo[] { const result: ReferencedEntityInfo[] = []; for (const entityTypeStr of Object.keys(references)) { const entityType = entityTypeStr as EntityType; @@ -127,7 +127,7 @@ export class ImageReferencesComponent implements OnInit { return result; } - private toReferencedEntitiesEntries(references: ImageReferences): Observable { + private toReferencedEntitiesEntries(references: ResourceReferences): Observable { let referencedEntities: ReferencedEntities = {}; const referencedEntitiesList = this.toReferencedEntitiesList(references); for (const referencedEntityInfo of referencedEntitiesList) { diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html similarity index 71% rename from ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html rename to ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html index 4f4667ceb8..08a35aa412 100644 --- a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html @@ -15,10 +15,10 @@ limitations under the License. --> -

{{title}}

-
-
- +

{{configuration.title}}

+
+
+
@@ -28,14 +28,14 @@ [indeterminate]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async) === false"> - + + (change)="$event ? dataSource.selection.toggle(resource) : null" + [checked]="dataSource.selection.isSelected(resource)"> - + {{ image.title }} @@ -43,30 +43,30 @@ - - {{ 'image.name' | translate }} + + {{ 'resource.title' | translate }} - - {{ translate.get('image.selected-images', {count: dataSource.selection.selected.length}) | async }} + + {{ translate.get(configuration.selectedText, {count: dataSource.selection.selected.length}) | async }} - - {{ image.title }} + + {{ resource.title }} - + + (click)="toggleShowReferences($event, resource, showReferencesButton)">{{ 'image.references' | translate }} - + + [class.mat-selected]="dataSource.selection.isSelected(resource)" + *matRowDef="let resource; columns: configuration.columns;">
@@ -76,7 +76,7 @@
- + -
+
diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss similarity index 96% rename from ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss rename to ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss index 5c053e984f..380a6070eb 100644 --- a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss @@ -14,7 +14,7 @@ * limitations under the License. */ :host { - .tb-images-in-use-content { + .tb-resources-in-use-content { display: flex; flex-direction: column; gap: 24px; diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts similarity index 66% rename from ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts rename to ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts index 681e9a2c53..6f83fded7d 100644 --- a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts @@ -20,37 +20,50 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; -import { ImageReferences, ImageResourceInfo, ImageResourceInfoWithReferences } from '@shared/models/resource.models'; -import { ImagesDatasource } from '@shared/components/image/images-datasource'; +import { + ResourceReferences, + ResourceInfoWithReferences, + ResourceInfo +} from '@shared/models/resource.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { ImageReferencesComponent } from '@shared/components/image/image-references.component'; import { TranslateService } from '@ngx-translate/core'; +import { Datasource } from "@shared/models/widget.models"; -export interface ImagesInUseDialogData { +interface ResourcesInUseDialogDataConfiguration { + title: string; + message: string; + columns: string[]; + deleteText: string; + selectedText: string; + datasource?: Datasource; +} + +export interface ResourcesInUseDialogData { multiple: boolean; - images: ImageResourceInfoWithReferences[]; + resources: ResourceInfoWithReferences[]; + configuration: ResourcesInUseDialogDataConfiguration; } @Component({ - selector: 'tb-images-in-use-dialog', - templateUrl: './images-in-use-dialog.component.html', - styleUrls: ['./images-in-use-dialog.component.scss'] + selector: 'tb-resources-in-use-dialog', + templateUrl: './resources-in-use-dialog.component.html', + styleUrls: ['./resources-in-use-dialog.component.scss'] }) -export class ImagesInUseDialogComponent extends - DialogComponent implements OnInit { - - title: string; - message: string; +export class ResourcesInUseDialogComponent extends + DialogComponent implements OnInit { - references: ImageReferences; + displayPreview: boolean; + configuration: ResourcesInUseDialogDataConfiguration; + references: ResourceReferences; - dataSource: ImagesDatasource; + dataSource: Datasource; constructor(protected store: Store, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: ImagesInUseDialogData, - public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ResourcesInUseDialogData, + public dialogRef: MatDialogRef, public translate: TranslateService, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, @@ -59,14 +72,12 @@ export class ImagesInUseDialogComponent extends } ngOnInit(): void { + this.configuration = this.data.configuration; + this.displayPreview = this.data.configuration.columns.includes('preview'); if (this.data.multiple) { - this.title = this.translate.instant('image.images-are-in-use'); - this.message = this.translate.instant('image.images-are-in-use-text'); - this.dataSource = new ImagesDatasource(null, this.data.images, entity => true); + this.dataSource = this.data.configuration.datasource; } else { - this.title = this.translate.instant('image.image-is-in-use'); - this.message = this.translate.instant('image.image-is-in-use-text', {title: this.data.images[0].title}); - this.references = this.data.images[0].references; + this.references = this.data.resources[0].references; } } @@ -78,11 +89,11 @@ export class ImagesInUseDialogComponent extends if (this.data.multiple) { this.dialogRef.close(this.dataSource.selection.selected); } else { - this.dialogRef.close(this.data.images); + this.dialogRef.close(this.data.resources); } } - toggleShowReferences($event: Event, image: ImageResourceInfoWithReferences, referencesButton: MatButton) { + toggleShowReferences($event: Event, resource: ResourceInfoWithReferences, referencesButton: MatButton) { if ($event) { $event.stopPropagation(); } @@ -93,7 +104,7 @@ export class ImagesInUseDialogComponent extends const referencesPopover = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, ImageReferencesComponent, 'top', true, null, { - references: image.references + references: resource.references }, {}, {}, {}, false, visible => { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index eda0182383..0419e40a7c 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -120,28 +120,28 @@ export interface ImageExportData { export type ImageResourceType = 'tenant' | 'system'; export type TBResourceScope = 'tenant' | 'system'; -export type ImageReferences = {[entityType: string]: Array & HasTenantId>}; +export type ResourceReferences = {[entityType: string]: Array & HasTenantId>}; -export interface ImageResourceInfoWithReferences extends ImageResourceInfo { - references: ImageReferences; +export interface ResourceInfoWithReferences extends ResourceInfo { + references: ResourceReferences; } -export interface ImageDeleteResult { - image: ImageResourceInfo; +export interface ResourceDeleteResult { + resource: TbResourceInfo; success: boolean; - imageIsReferencedError?: boolean; + resourceIsReferencedError?: boolean; error?: any; - references?: ImageReferences; + references?: ResourceReferences; } -export const toImageDeleteResult = (image: ImageResourceInfo, e?: any): ImageDeleteResult => { +export const toResourceDeleteResult = (resource: ResourceInfo, e?: any): ResourceDeleteResult => { if (!e) { - return {image, success: true}; + return {resource, success: true}; } else { - const result: ImageDeleteResult = {image, success: false, error: e}; + const result: ResourceDeleteResult = {resource, success: false, error: e}; if (e?.status === 400 && e?.error?.success === false && e?.error?.references) { - const references: ImageReferences = e?.error?.references; - result.imageIsReferencedError = true; + const references: ResourceReferences = e?.error?.references; + result.resourceIsReferencedError = true; result.references = references; } return result; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9245673d89..9b02d49c8f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -201,7 +201,7 @@ import { ImageGalleryComponent } from '@shared/components/image/image-gallery.co import { UploadImageDialogComponent } from '@shared/components/image/upload-image-dialog.component'; import { ImageDialogComponent } from '@shared/components/image/image-dialog.component'; import { ImageReferencesComponent } from '@shared/components/image/image-references.component'; -import { ImagesInUseDialogComponent } from '@shared/components/image/images-in-use-dialog.component'; +import { ResourcesInUseDialogComponent } from '@shared/components/resource/resources-in-use-dialog.component'; import { GalleryImageInputComponent } from '@shared/components/image/gallery-image-input.component'; import { MultipleGalleryImageInputComponent } from '@shared/components/image/multiple-gallery-image-input.component'; import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component'; @@ -425,7 +425,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) UploadImageDialogComponent, ImageDialogComponent, ImageReferencesComponent, - ImagesInUseDialogComponent, + ResourcesInUseDialogComponent, GalleryImageInputComponent, MultipleGalleryImageInputComponent, EmbedImageDialogComponent, @@ -688,7 +688,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) UploadImageDialogComponent, ImageDialogComponent, ImageReferencesComponent, - ImagesInUseDialogComponent, + ResourcesInUseDialogComponent, GalleryImageInputComponent, MultipleGalleryImageInputComponent, EmbedImageDialogComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 18801bda2c..4f5d14f2f6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4265,6 +4265,7 @@ "delete-javascript-resources-action-title": "Delete JavaScript { count, plural, =1 {1 resource} other {# resources} }", "delete-javascript-resources-text": "Please note that the selected JavaScript resources, even if they are used in JavaScript functions, will be deleted.", "delete-javascript-resources-title": "Are you sure you want to delete JavaScript { count, plural, =1 {1 resource} other {# resources} }?", + "delete-javascript-resource-in-use-text": "If you still want to delete the JavaScript resource, click the Delete anyway button.", "download": "Download JavaScript resource", "upload-from-file": "Upload JavaScript from file", "resource-file": "JavaScript resource file", @@ -4273,6 +4274,10 @@ "javascript-library": "JavaScript library", "javascript-type": "JavaScript type", "javascript-resource-details": "JavaScript resource details", + "javascript-resource-is-in-use": "JavaScript resource is used by other entities", + "javascript-resources-are-in-use": "JavaScript resources are used by other entities", + "javascript-resource-is-in-use-text": "The JavaScript resource '{{title}}' was not deleted because it is used by the following entities:", + "javascript-resources-are-in-use-text": "Not all JavaScript resources have been deleted because they are used by other entities.
You can view referenced entities by clicking the References button in the corresponding resource row.
If you still want to delete these JavaScript resources, select them in the table below and click the Delete selected button.", "search": "Search JavaScript resources", "selected-javascript-resources": "{ count, plural, =1 {1 JavaScript resource} other {# JavaScript resources} } selected", "no-javascript-resource-text": "No JavaScript resources found", @@ -4730,10 +4735,6 @@ "min-buffer-memory-message": "Only 0 minimum buffer size is allowed.", "memory-buffer-size-range": "Memory buffer size must be between 0 and {{max}} KB", "acks": "Number of acknowledgments", - "key-serializer": "Key serializer", - "key-serializer-required": "Key serializer is required", - "value-serializer": "Value serializer", - "value-serializer-required": "Value serializer is required", "topic-arn-pattern": "Topic ARN pattern", "topic-arn-pattern-required": "Topic ARN pattern is required", "aws-access-key-id": "AWS Access Key ID",